Merge branch 'main' into sql_server_extra_data_components
This commit is contained in:
@@ -45,7 +45,7 @@ jobs:
|
||||
go-version: "1.27"
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -56,4 +56,4 @@ jobs:
|
||||
run: |
|
||||
go build .
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4
|
||||
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
|
||||
- name: Configure AWS credentials
|
||||
if: steps.detect.outputs.any_changed == 'true'
|
||||
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6
|
||||
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9
|
||||
with:
|
||||
# NOTE: Version and args must match scripts/lint.sh
|
||||
version: v2.13.1
|
||||
version: v2.13.2
|
||||
args: --enable bodyclose,copyloopvar,misspell --timeout 10m
|
||||
man-page-staleness:
|
||||
name: man-page-staleness
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
||||
uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4
|
||||
- name: Docker Login to DockerHub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||
with:
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
secret: ${{ secrets.BUILDPULSE_SECRET_ACCESS_KEY }}
|
||||
tags: integration
|
||||
- name: Annotate test results
|
||||
uses: mikepenz/action-junit-report@d9f48fc87bc235f7e214acf696ca5abc0a986f16 # v6
|
||||
uses: mikepenz/action-junit-report@a9170d5795813c01ab4901ffb045b52bab4ab09d # v6
|
||||
if: success() || failure() # always run even if the previous step fails
|
||||
with:
|
||||
report_paths: "tmp/test-results/*.xml"
|
||||
|
||||
@@ -640,6 +640,58 @@ Multiple roles can be passed as separate arguments. The following command will a
|
||||
trufflehog s3 --role-arn=<iam-role-arn-1> --role-arn=<iam-role-arn-2>
|
||||
```
|
||||
|
||||
### Narrowing a scan to specific objects
|
||||
|
||||
Large buckets often hold data that is never worth scanning, such as archives, media, and build artifacts. Two sets of flags keep those objects out of a scan. Both are matched against the object key before the object is downloaded, so a skipped object costs no GET request.
|
||||
|
||||
Scan only the objects under one or more key prefixes:
|
||||
|
||||
```bash
|
||||
trufflehog s3 --bucket=<bucket-name> --include-prefix=infra/ --include-prefix=services/
|
||||
```
|
||||
|
||||
Skip the objects under a key prefix:
|
||||
|
||||
```bash
|
||||
trufflehog s3 --bucket=<bucket-name> --exclude-prefix=projects/archived/
|
||||
```
|
||||
|
||||
Both prefix flags can be used together to scan a subtree while leaving one part of it out. An object that matches an exclude prefix is always skipped, even if it also matches an include prefix:
|
||||
|
||||
```bash
|
||||
trufflehog s3 --bucket=<bucket-name> --include-prefix=src/ --exclude-prefix=src/vendor/
|
||||
```
|
||||
|
||||
Prefixes are matched literally, not as globs, and they are not confined to a path boundary. `--include-prefix=log` matches both `logs/app.txt` and `logs-archive/app.txt`.
|
||||
|
||||
File extensions are filtered separately. Write them without a leading dot:
|
||||
|
||||
```bash
|
||||
trufflehog s3 --bucket=<bucket-name> --exclude-extension=zip --exclude-extension=mp4
|
||||
```
|
||||
|
||||
```bash
|
||||
trufflehog s3 --bucket=<bucket-name> --include-extension=tf --include-extension=yaml
|
||||
```
|
||||
|
||||
Unlike the prefix flags, `--include-extension` and `--exclude-extension` cannot be combined, because naming the extensions to scan already excludes every other one. Using both fails at startup.
|
||||
|
||||
Extension matching is case insensitive, so `--exclude-extension=zip` also skips `BACKUP.ZIP`. An object whose key has no extension at all, such as `Makefile`, matches no entry: it is skipped when `--include-extension` is set, and kept when only `--exclude-extension` is set.
|
||||
|
||||
Two things to watch when using `--include-extension`. A dotfile counts as all extension, so `.env` has the extension `env` and any include list that leaves out `env` will skip every `.env` file in the bucket. Since `.env` files are a common place for secrets to sit, add `--include-extension=env` unless you mean to skip them. And only the last extension counts, so `backup.tar.gz` has the extension `gz`: `--exclude-extension=tar.gz` matches nothing, while `--exclude-extension=gz` works.
|
||||
|
||||
These flags also apply to object keys only, not to files inside an archive. `--exclude-extension=mp4` skips `clip.mp4` sitting in the bucket, but not a `clip.mp4` packed inside `media.zip`. Excluding `zip` skips the archive entirely.
|
||||
|
||||
Filtering happens after TruffleHog lists a bucket, so it cuts the cost of downloading objects but not the cost of listing them. A bucket is still paginated in full even when a prefix covers a small part of it.
|
||||
|
||||
Prefixes and extensions are applied together. An object has to pass both to be scanned, so the command below scans `src/main.tf` but skips both `src/bundle.zip` and `docs/guide.tf`:
|
||||
|
||||
```bash
|
||||
trufflehog s3 --bucket=<bucket-name> --include-prefix=src/ --exclude-extension=zip
|
||||
```
|
||||
|
||||
These flags narrow the objects within a bucket. To narrow which buckets are scanned, use `--bucket` or `--ignore-bucket`, which unlike the prefix flags cannot be used together.
|
||||
|
||||
Exit Codes:
|
||||
|
||||
- 0: No errors and no results were found.
|
||||
|
||||
@@ -335,6 +335,18 @@ Endpoint of an S3-compatible service to scan instead of AWS S3. (eg. https://s3.
|
||||
.TP
|
||||
\fB--region=REGION\fR
|
||||
Region used to sign requests. Defaults to us-east-1.
|
||||
.TP
|
||||
\fB--include-prefix=INCLUDE-PREFIX\fR
|
||||
Only scan objects whose key starts with this prefix. You can repeat this flag.
|
||||
.TP
|
||||
\fB--exclude-prefix=EXCLUDE-PREFIX\fR
|
||||
Skip objects whose key starts with this prefix. You can repeat this flag. Takes precedence over --include-prefix.
|
||||
.TP
|
||||
\fB--include-extension=INCLUDE-EXTENSION\fR
|
||||
Only scan objects with this file extension, written without a leading dot (eg. tf). You can repeat this flag. Incompatible with --exclude-extension.
|
||||
.TP
|
||||
\fB--exclude-extension=EXCLUDE-EXTENSION\fR
|
||||
Skip objects with this file extension, written without a leading dot (eg. zip). You can repeat this flag. Incompatible with --include-extension.
|
||||
.SS
|
||||
\fBgcs [<flags>]\fR
|
||||
Find credentials in GCS buckets.
|
||||
|
||||
@@ -64,6 +64,7 @@ require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/jpillora/overseer v1.1.6
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
|
||||
github.com/klauspost/compress v1.18.6
|
||||
github.com/klauspost/pgzip v1.2.6
|
||||
github.com/kylelemons/godebug v1.1.0
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.12
|
||||
@@ -80,7 +81,7 @@ require (
|
||||
github.com/paulbellamy/ratecounter v0.2.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/rabbitmq/amqp091-go v1.13.0
|
||||
github.com/repeale/fp-go v0.11.1
|
||||
github.com/sassoftware/go-rpmutils v0.4.0
|
||||
github.com/schollz/progressbar/v3 v3.17.1
|
||||
@@ -232,7 +233,6 @@ require (
|
||||
github.com/jpillora/s3 v1.1.4 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
|
||||
@@ -646,8 +646,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rabbitmq/amqp091-go v1.13.0 h1:L8NA1WtF76C6KA3LAoufjfLgbist/If1UQYcsOjtxXA=
|
||||
github.com/rabbitmq/amqp091-go v1.13.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/repeale/fp-go v0.11.1 h1:Q/e+gNyyHaxKAyfdbBqvip3DxhVWH453R+kthvSr9Mk=
|
||||
github.com/repeale/fp-go v0.11.1/go.mod h1:4KrwQJB1VRY+06CA+jTc4baZetr6o2PeuqnKr5ybQUc=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
|
||||
@@ -179,6 +179,11 @@ var (
|
||||
s3ScanEndpoint = s3Scan.Flag("endpoint", "Endpoint of an S3-compatible service to scan instead of AWS S3. (eg. https://s3.internal.example.com)").String()
|
||||
s3ScanRegion = s3Scan.Flag("region", "Region used to sign requests. Defaults to us-east-1.").String()
|
||||
|
||||
s3ScanIncludePrefixes = s3Scan.Flag("include-prefix", "Only scan objects whose key starts with this prefix. You can repeat this flag.").Strings()
|
||||
s3ScanExcludePrefixes = s3Scan.Flag("exclude-prefix", "Skip objects whose key starts with this prefix. You can repeat this flag. Takes precedence over --include-prefix.").Strings()
|
||||
s3ScanIncludeExtensions = s3Scan.Flag("include-extension", "Only scan objects with this file extension, written without a leading dot (eg. tf). You can repeat this flag. Incompatible with --exclude-extension.").Strings()
|
||||
s3ScanExcludeExtensions = s3Scan.Flag("exclude-extension", "Skip objects with this file extension, written without a leading dot (eg. zip). You can repeat this flag. Incompatible with --include-extension.").Strings()
|
||||
|
||||
gcsScan = cli.Command("gcs", "Find credentials in GCS buckets.")
|
||||
gcsProjectID = gcsScan.Flag("project-id", "GCS project ID used to authenticate. Can NOT be used with unauth scan. Can be provided with environment variable GOOGLE_CLOUD_PROJECT.").Envar("GOOGLE_CLOUD_PROJECT").String()
|
||||
gcsCloudEnv = gcsScan.Flag("cloud-environment", "Use Application Default Credentials, IAM credentials to authenticate.").Bool()
|
||||
@@ -575,7 +580,9 @@ func run(state overseer.State, logSync func() error) {
|
||||
feature.NewRelicMobileAppTokenDetectorEnabled.Store(true)
|
||||
feature.MSTeamsWebhookV2DetectorEnabled.Store(true)
|
||||
feature.SolarwindsDetectorEnabled.Store(true)
|
||||
feature.ResendDetectorEnabled.Store(true)
|
||||
feature.WeightsAndBiasesV2DetectorEnabled.Store(true)
|
||||
feature.HumioAPITokenDetectorEnabled.Store(true)
|
||||
|
||||
conf := &config.Config{}
|
||||
if *configFilename != "" {
|
||||
@@ -1021,16 +1028,20 @@ func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics,
|
||||
}
|
||||
case s3Scan.FullCommand():
|
||||
cfg := sources.S3Config{
|
||||
Key: *s3ScanKey,
|
||||
Secret: *s3ScanSecret,
|
||||
SessionToken: *s3ScanSessionToken,
|
||||
Buckets: *s3ScanBuckets,
|
||||
IgnoreBuckets: *s3ScanIgnoreBuckets,
|
||||
Roles: *s3ScanRoleArns,
|
||||
CloudCred: *s3ScanCloudEnv,
|
||||
MaxObjectSize: int64(*s3ScanMaxObjectSize),
|
||||
Endpoint: *s3ScanEndpoint,
|
||||
Region: *s3ScanRegion,
|
||||
Key: *s3ScanKey,
|
||||
Secret: *s3ScanSecret,
|
||||
SessionToken: *s3ScanSessionToken,
|
||||
Buckets: *s3ScanBuckets,
|
||||
IgnoreBuckets: *s3ScanIgnoreBuckets,
|
||||
Roles: *s3ScanRoleArns,
|
||||
CloudCred: *s3ScanCloudEnv,
|
||||
MaxObjectSize: int64(*s3ScanMaxObjectSize),
|
||||
Endpoint: *s3ScanEndpoint,
|
||||
Region: *s3ScanRegion,
|
||||
IncludePrefixes: *s3ScanIncludePrefixes,
|
||||
ExcludePrefixes: *s3ScanExcludePrefixes,
|
||||
IncludeExtensions: *s3ScanIncludeExtensions,
|
||||
ExcludeExtensions: *s3ScanExcludeExtensions,
|
||||
}
|
||||
if ref, err := eng.ScanS3(ctx, cfg); err != nil {
|
||||
return scanMetrics, fmt.Errorf("failed to scan S3: %v", err)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/github"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/gitlab"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/jenkins"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/json_enumerator"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/postman"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/s3"
|
||||
)
|
||||
@@ -97,6 +98,8 @@ func instantiateSourceFromType(sourceType string) (sources.Source, error) {
|
||||
source = new(filesystem.Source)
|
||||
case sourcespb.SourceType_SOURCE_TYPE_JENKINS.String():
|
||||
source = new(jenkins.Source)
|
||||
case sourcespb.SourceType_SOURCE_TYPE_JSON_ENUMERATOR.String():
|
||||
source = new(json_enumerator.Source)
|
||||
case sourcespb.SourceType_SOURCE_TYPE_GCS.String():
|
||||
source = new(gcs.Source)
|
||||
case sourcespb.SourceType_SOURCE_TYPE_GCS_UNAUTHED.String():
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package humioapitoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
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 {
|
||||
client *http.Client
|
||||
detectors.EndpointSetter
|
||||
}
|
||||
|
||||
var _ detectors.Detector = (*Scanner)(nil)
|
||||
var _ detectors.EndpointCustomizer = (*Scanner)(nil)
|
||||
var _ detectors.CloudProvider = (*Scanner)(nil)
|
||||
|
||||
func (Scanner) CloudEndpoint() string { return "https://cloud.us.humio.com" }
|
||||
|
||||
// additionalCloudEndpoints lists Humio/LogScale SaaS regions beyond the
|
||||
// primary one returned by CloudEndpoint(). The CloudProvider interface
|
||||
// only supports a single endpoint, so these are passed as found
|
||||
// endpoints during verification to ensure tokens from any region are
|
||||
// verified even when the scanned data contains no URL.
|
||||
var additionalCloudEndpoints = []string{
|
||||
"https://cloud.humio.com",
|
||||
}
|
||||
|
||||
var (
|
||||
defaultClient = common.SaneHttpClient()
|
||||
|
||||
// API tokens are base62 strings with a tilde (~) separator: a 24- or
|
||||
// 32-character prefix and a fixed 44-character suffix. The tilde +
|
||||
// exact segment lengths make this format distinctive enough that no
|
||||
// PrefixRegex or surrounding-context anchor is needed.
|
||||
// Example: pHUw1oLASALFmt2ppNvwCR0Meo2nHQ15~ECViF0Ce95uIqFGSSatjKWX71EOzvkpVGSuc3zGYqJgR
|
||||
keyPat = regexp.MustCompile(`\b([A-Za-z0-9]{24}(?:[A-Za-z0-9]{8})?~[A-Za-z0-9]{44})\b`)
|
||||
)
|
||||
|
||||
func (s Scanner) Type() detector_typepb.DetectorType {
|
||||
return detector_typepb.DetectorType_HumioAPIToken
|
||||
}
|
||||
|
||||
func (s Scanner) Keywords() []string {
|
||||
// The tilde separator in the token format is distinctive but too common
|
||||
// in source code (URLs, paths, shell, bitwise ops) to use as a keyword.
|
||||
// Te regex handles precision within matched chunks.
|
||||
return []string{"humio", "logscale"}
|
||||
}
|
||||
|
||||
func (s Scanner) Description() string {
|
||||
return "Humio/CrowdStrike Falcon LogScale is a log management platform. API tokens grant read/write access to a specific LogScale repository via the REST and GraphQL APIs."
|
||||
}
|
||||
|
||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||
dataStr := string(data)
|
||||
|
||||
tokenMatches := make(map[string]struct{})
|
||||
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
t := strings.TrimSpace(match[1])
|
||||
tokenMatches[t] = struct{}{}
|
||||
}
|
||||
|
||||
for token := range tokenMatches {
|
||||
r := detectors.Result{
|
||||
DetectorType: detector_typepb.DetectorType_HumioAPIToken,
|
||||
Raw: []byte(token),
|
||||
SecretParts: map[string]string{"key": token},
|
||||
ExtraData: map[string]string{"token_type": humioTokenType(token)},
|
||||
}
|
||||
|
||||
if verify {
|
||||
client := s.client
|
||||
if client == nil {
|
||||
client = defaultClient
|
||||
}
|
||||
|
||||
// Verification only targets known SaaS regions (US via
|
||||
// CloudEndpoint, EU via additionalCloudEndpoints) plus any
|
||||
// endpoints explicitly configured by the operator (e.g.
|
||||
// self-hosted instances). URLs discovered in scanned data are
|
||||
// not used — arbitrary hosts can return 403 for non-auth
|
||||
// reasons (WAF, CDN), causing false verification.
|
||||
//
|
||||
// Only non-nil errors overwrite lastErr so that a clean 401
|
||||
// from the wrong region doesn't erase a transient failure
|
||||
// (500, timeout) from a region that might be authoritative.
|
||||
var lastErr error
|
||||
for _, baseURL := range s.Endpoints(additionalCloudEndpoints...) {
|
||||
isVerified, vErr := verifyAPIToken(ctx, client, baseURL, token)
|
||||
r.Verified = isVerified
|
||||
if vErr != nil {
|
||||
lastErr = vErr
|
||||
}
|
||||
if isVerified {
|
||||
lastErr = nil
|
||||
r.ExtraData["endpoint"] = baseURL
|
||||
break
|
||||
}
|
||||
}
|
||||
r.SetVerificationError(lastErr, token)
|
||||
}
|
||||
|
||||
results = append(results, r)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// humioTokenType classifies the token based on prefix length: 24-char
|
||||
// prefixes are personal API tokens (PATs), 32-char prefixes are
|
||||
// repository/view API tokens.
|
||||
func humioTokenType(token string) string {
|
||||
idx := strings.Index(token, "~")
|
||||
if idx == 24 {
|
||||
return "Personal API Token"
|
||||
}
|
||||
return "Repository API Token"
|
||||
}
|
||||
|
||||
// verifyAPIToken hits the health-json endpoint, which is read-only and
|
||||
// produces no side effects. The endpoint requires authentication and
|
||||
// returns cluster health status on success.
|
||||
//
|
||||
// Response semantics: 200 means fully valid. 403 signals a recognized token
|
||||
// blocked by an IP filter, so it still counts as verified. 401 means the
|
||||
// token is unknown or expired.
|
||||
func verifyAPIToken(ctx context.Context, client *http.Client, baseURL, token string) (bool, error) {
|
||||
endpoint, err := url.JoinPath(baseURL, "/api/v1/health-json")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusForbidden:
|
||||
// Token was recognized but blocked (IP filter, permission change).
|
||||
return true, nil
|
||||
case http.StatusUnauthorized:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
//go:build detectors
|
||||
// +build detectors
|
||||
|
||||
package humioapitoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
||||
)
|
||||
|
||||
// newTestScanner returns a Scanner with the cloud endpoint configured, mirroring
|
||||
// what DefaultDetectors() does at startup. Without this, Endpoints() returns
|
||||
// nothing and the verification loop never runs.
|
||||
func newTestScanner(client *http.Client) Scanner {
|
||||
s := Scanner{client: client}
|
||||
s.SetCloudEndpoint("https://cloud.us.humio.com")
|
||||
s.UseCloudEndpoint(true)
|
||||
return s
|
||||
}
|
||||
|
||||
func TestHumioAPIToken_FromChunk(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors7")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
secret := testSecrets.MustGetField("HUMIOAPITOKEN")
|
||||
inactiveSecret := testSecrets.MustGetField("HUMIOAPITOKEN_INACTIVE")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
data []byte
|
||||
verify bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
s Scanner
|
||||
args args
|
||||
want []detectors.Result
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "found, verified",
|
||||
s: newTestScanner(nil),
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a humio api token %s within", secret)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_HumioAPIToken,
|
||||
Verified: true,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, real secrets, verification error due to timeout",
|
||||
s: newTestScanner(common.SaneHttpClientTimeOut(1 * time.Microsecond)),
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a humio api token %s within", secret)),
|
||||
verify: true,
|
||||
},
|
||||
want: func() []detectors.Result {
|
||||
r := detectors.Result{
|
||||
DetectorType: detector_typepb.DetectorType_HumioAPIToken,
|
||||
Verified: false,
|
||||
}
|
||||
r.SetVerificationError(context.DeadlineExceeded)
|
||||
return []detectors.Result{r}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, real secrets, verification error due to unexpected api surface",
|
||||
s: newTestScanner(common.ConstantResponseHttpClient(500, "{}")),
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a humio api token %s within", secret)),
|
||||
verify: true,
|
||||
},
|
||||
want: func() []detectors.Result {
|
||||
r := detectors.Result{
|
||||
DetectorType: detector_typepb.DetectorType_HumioAPIToken,
|
||||
Verified: false,
|
||||
}
|
||||
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
|
||||
return []detectors.Result{r}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, unverified",
|
||||
s: newTestScanner(nil),
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a humio api token %s within but not valid", inactiveSecret)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_HumioAPIToken,
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
s: newTestScanner(nil),
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte("You cannot find the secret within"),
|
||||
verify: true,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("HumioAPIToken.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if len(got[i].Raw) == 0 {
|
||||
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||
}
|
||||
gotErr := ""
|
||||
if got[i].VerificationError() != nil {
|
||||
gotErr = got[i].VerificationError().Error()
|
||||
}
|
||||
wantErr := ""
|
||||
if tt.want[i].VerificationError() != nil {
|
||||
wantErr = tt.want[i].VerificationError().Error()
|
||||
}
|
||||
if gotErr != wantErr {
|
||||
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.want[i].VerificationError(), got[i].VerificationError())
|
||||
}
|
||||
}
|
||||
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "SecretParts", "ExtraData")
|
||||
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
|
||||
t.Errorf("HumioAPIToken.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromData(benchmark *testing.B) {
|
||||
ctx := context.Background()
|
||||
s := Scanner{}
|
||||
for name, data := range detectors.MustGetBenchmarkData() {
|
||||
benchmark.Run(name, func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, err := s.FromData(ctx, false, data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package humioapitoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
|
||||
)
|
||||
|
||||
var (
|
||||
// 32-char prefix + tilde + 44-char suffix (repository API token)
|
||||
validPattern32 = "pHUw1oLASALFmt2ppNvwCR0Meo2nHQ15~ECViF0Ce95uIqFGSSatjKWX71EOzvkpVGSuc3zGYqJgR"
|
||||
// 24-char prefix + tilde + 44-char suffix (personal API token)
|
||||
validPattern24 = "abcDefGhiJklMnoPqrStUvWx~ECViF0Ce95uIqFGSSatjKWX71EOzvkpVGSuc3zGYqJgR"
|
||||
)
|
||||
|
||||
func TestHumioAPIToken_Pattern(t *testing.T) {
|
||||
d := Scanner{}
|
||||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid pattern - python client config",
|
||||
input: `
|
||||
from humiolib.HumioClient import HumioClient
|
||||
|
||||
client = HumioClient(
|
||||
base_url="https://cloud.us.humio.com",
|
||||
api_token="pHUw1oLASALFmt2ppNvwCR0Meo2nHQ15~ECViF0Ce95uIqFGSSatjKWX71EOzvkpVGSuc3zGYqJgR",
|
||||
)
|
||||
results = client.search_repo("sandbox", "loglevel=ERROR")`,
|
||||
want: []string{validPattern32},
|
||||
},
|
||||
{
|
||||
name: "valid pattern - env file with PAT",
|
||||
input: `
|
||||
# LogScale personal access token for CI/CD pipeline
|
||||
LOGSCALE_API_TOKEN=abcDefGhiJklMnoPqrStUvWx~ECViF0Ce95uIqFGSSatjKWX71EOzvkpVGSuc3zGYqJgR
|
||||
LOGSCALE_BASE_URL=https://cloud.humio.com
|
||||
LOG_LEVEL=info`,
|
||||
want: []string{validPattern24},
|
||||
},
|
||||
{
|
||||
name: "valid pattern - logscale keyword in docker compose",
|
||||
input: `
|
||||
services:
|
||||
log-shipper:
|
||||
image: fluent/fluent-bit:latest
|
||||
environment:
|
||||
- LOGSCALE_TOKEN=pHUw1oLASALFmt2ppNvwCR0Meo2nHQ15~ECViF0Ce95uIqFGSSatjKWX71EOzvkpVGSuc3zGYqJgR
|
||||
- LOGSCALE_HOST=https://cloud.us.humio.com
|
||||
restart: always`,
|
||||
want: []string{validPattern32},
|
||||
},
|
||||
{
|
||||
name: "valid pattern - ignore duplicate",
|
||||
input: fmt.Sprintf("humio token = '%s' | '%s'", validPattern32, validPattern32),
|
||||
want: []string{validPattern32},
|
||||
},
|
||||
{
|
||||
name: "valid pattern - token far from keyword",
|
||||
input: `
|
||||
# Humio repository configuration
|
||||
# Generated by setup wizard on 2025-01-15
|
||||
|
||||
|
||||
api_token = "pHUw1oLASALFmt2ppNvwCR0Meo2nHQ15~ECViF0Ce95uIqFGSSatjKWX71EOzvkpVGSuc3zGYqJgR"`,
|
||||
want: []string{validPattern32},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - tilde present but suffix too short",
|
||||
input: `
|
||||
from humiolib.HumioClient import HumioClient
|
||||
client = HumioClient(api_token="pHUw1oLASALFmt2ppNvwCR0Meo2nHQ15~ECViF0Ce95uIqFGSS")`,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - tilde present but segments too short",
|
||||
input: `
|
||||
# humio config
|
||||
token = "abc~def"`,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - wrong prefix length (28 chars)",
|
||||
input: `
|
||||
# humio config
|
||||
token = "abcDefGhiJklMnoPqrStUvWxYzAb~ECViF0Ce95uIqFGSSatjKWX71EOzvkpVGSuc3zGYqJgR"`,
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
|
||||
if len(matchedDetectors) == 0 {
|
||||
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
|
||||
return
|
||||
}
|
||||
|
||||
results, err := d.FromData(context.Background(), false, []byte(test.input))
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(results) != len(test.want) {
|
||||
t.Errorf("expected %d results, got %d", len(test.want), len(results))
|
||||
return
|
||||
}
|
||||
|
||||
actual := make(map[string]struct{}, len(results))
|
||||
for _, r := range results {
|
||||
if len(r.RawV2) > 0 {
|
||||
actual[string(r.RawV2)] = struct{}{}
|
||||
} else {
|
||||
actual[string(r.Raw)] = struct{}{}
|
||||
}
|
||||
}
|
||||
expected := make(map[string]struct{}, len(test.want))
|
||||
for _, v := range test.want {
|
||||
expected[v] = struct{}{}
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise the verification logic against a mock health-json server. Each
|
||||
// HTTP status code maps to a specific verified/error outcome — 403 counts as
|
||||
// verified because the server recognized the token (just blocked by IP filter).
|
||||
func TestHumioAPIToken_Verification(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
wantVerified bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"200 - valid token", http.StatusOK, true, false},
|
||||
{"401 - unknown token", http.StatusUnauthorized, false, false},
|
||||
{"403 - recognized but blocked", http.StatusForbidden, true, false},
|
||||
{"500 - server error", http.StatusInternalServerError, false, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(tt.statusCode)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
s := Scanner{}
|
||||
s.SetCloudEndpoint(ts.URL)
|
||||
s.UseCloudEndpoint(true)
|
||||
|
||||
input := fmt.Sprintf("humio_token = '%s'", validPattern32)
|
||||
results, err := s.FromData(context.Background(), true, []byte(input))
|
||||
if err != nil {
|
||||
t.Fatalf("FromData error: %v", err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("expected 1 result, got %d", len(results))
|
||||
}
|
||||
|
||||
r := results[0]
|
||||
if r.Verified != tt.wantVerified {
|
||||
t.Errorf("Verified = %v, want %v", r.Verified, tt.wantVerified)
|
||||
}
|
||||
if tt.wantErr && r.VerificationError() == nil {
|
||||
t.Error("expected verification error, got nil")
|
||||
}
|
||||
if !tt.wantErr && r.VerificationError() != nil {
|
||||
t.Errorf("unexpected verification error: %v", r.VerificationError())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that a successful verification on a later endpoint clears any
|
||||
// error from an earlier failed attempt (e.g. first endpoint returns 500,
|
||||
// second returns 200).
|
||||
func TestHumioAPIToken_Verification_StaleErrorCleared(t *testing.T) {
|
||||
callCount := 0
|
||||
ts500 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer ts500.Close()
|
||||
|
||||
ts200 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts200.Close()
|
||||
|
||||
s := Scanner{}
|
||||
_ = s.SetConfiguredEndpoints(ts500.URL, ts200.URL)
|
||||
|
||||
input := fmt.Sprintf("humio_token = '%s'", validPattern32)
|
||||
results, err := s.FromData(context.Background(), true, []byte(input))
|
||||
if err != nil {
|
||||
t.Fatalf("FromData error: %v", err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("expected 1 result, got %d", len(results))
|
||||
}
|
||||
|
||||
r := results[0]
|
||||
if !r.Verified {
|
||||
t.Error("expected Verified = true after second endpoint succeeded")
|
||||
}
|
||||
if r.VerificationError() != nil {
|
||||
t.Errorf("stale verification error not cleared: %v", r.VerificationError())
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that a clean 401 from the wrong region does not erase a transient
|
||||
// error from an earlier endpoint. The verification error should survive so
|
||||
// consumers know the result is uncertain, not definitively "not valid."
|
||||
func TestHumioAPIToken_Verification_ErrorPreservedAcross401(t *testing.T) {
|
||||
ts500 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer ts500.Close()
|
||||
|
||||
ts401 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
defer ts401.Close()
|
||||
|
||||
s := Scanner{}
|
||||
_ = s.SetConfiguredEndpoints(ts500.URL, ts401.URL)
|
||||
|
||||
input := fmt.Sprintf("humio_token = '%s'", validPattern32)
|
||||
results, err := s.FromData(context.Background(), true, []byte(input))
|
||||
if err != nil {
|
||||
t.Fatalf("FromData error: %v", err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("expected 1 result, got %d", len(results))
|
||||
}
|
||||
|
||||
r := results[0]
|
||||
if r.Verified {
|
||||
t.Error("expected Verified = false")
|
||||
}
|
||||
if r.VerificationError() == nil {
|
||||
t.Error("expected verification error to be preserved after 401 from another endpoint, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHumioAPIToken_TokenType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
want string
|
||||
}{
|
||||
{"24-char prefix is Personal API Token", validPattern24, "Personal API Token"},
|
||||
{"32-char prefix is Repository API Token", validPattern32, "Repository API Token"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := humioTokenType(tt.token)
|
||||
if got != tt.want {
|
||||
t.Errorf("humioTokenType() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Data without humio/logscale context must not reach this detector — the
|
||||
// Aho-Corasick pre-filter should reject the chunk.
|
||||
func TestHumioAPIToken_NoKeywordMatch(t *testing.T) {
|
||||
d := Scanner{}
|
||||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
|
||||
|
||||
input := fmt.Sprintf("some_generic_token = '%s'", validPattern32)
|
||||
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(input))
|
||||
if len(matchedDetectors) != 0 {
|
||||
t.Errorf("expected no keyword match for input without humio/logscale context, got %d", len(matchedDetectors))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package resend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
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 {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// Ensure the Scanner satisfies the interface at compile time.
|
||||
var _ detectors.Detector = (*Scanner)(nil)
|
||||
|
||||
// GET /api-keys verifies both Resend permission tiers with a single
|
||||
// read-only request:
|
||||
// - full_access -> 200
|
||||
// - sending_access -> 401 with body {"name":"restricted_api_key"}
|
||||
//
|
||||
// See https://resend.com/docs/api-reference/api-keys/list-api-keys and
|
||||
// https://resend.com/docs/api-reference/errors.
|
||||
const verifyURL = "https://api.resend.com/api-keys"
|
||||
|
||||
var (
|
||||
defaultClient = common.SaneHttpClient()
|
||||
|
||||
// Resend API keys: prefix `re_`, 8 chars of base58,
|
||||
// underscore, then 24 chars from the same alphabet.
|
||||
keyPat = regexp.MustCompile(`\b(re_[1-9A-HJ-NP-Za-km-z]{8}_[1-9A-HJ-NP-Za-km-z]{24})\b`)
|
||||
)
|
||||
|
||||
// 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{"re_"}
|
||||
}
|
||||
|
||||
// FromData will find and optionally verify Resend 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)
|
||||
|
||||
uniqueMatches := make(map[string]struct{})
|
||||
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
uniqueMatches[match[1]] = struct{}{}
|
||||
}
|
||||
|
||||
for token := range uniqueMatches {
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detector_typepb.DetectorType_Resend,
|
||||
Raw: []byte(token),
|
||||
SecretParts: map[string]string{"key": token},
|
||||
}
|
||||
|
||||
if verify {
|
||||
client := s.getClient()
|
||||
isVerified, extraData, verificationErr := verifyMatch(ctx, client, token)
|
||||
s1.Verified = isVerified
|
||||
s1.ExtraData = extraData
|
||||
s1.SetVerificationError(verificationErr, token)
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s Scanner) getClient() *http.Client {
|
||||
if s.client != nil {
|
||||
return s.client
|
||||
}
|
||||
return defaultClient
|
||||
}
|
||||
|
||||
type resendError struct {
|
||||
Name string `json:"name"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, map[string]string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, verifyURL, nil)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
extraData := map[string]string{
|
||||
"rotation_guide": "https://resend.com/docs/dashboard/api-keys/introduction",
|
||||
}
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
// Active key with `full_access` permission.
|
||||
extraData["permission"] = "full_access"
|
||||
return true, extraData, nil
|
||||
|
||||
case http.StatusUnauthorized:
|
||||
// Resend returns 401 for two distinct cases. We must look at the
|
||||
// error name to tell them apart:
|
||||
// - restricted_api_key: active `sending_access` key trying to
|
||||
// reach a non-send route. The key is real and can send mail.
|
||||
// - missing_api_key: no Authorization header (should not
|
||||
// happen here since we set one above).
|
||||
var body resendError
|
||||
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
|
||||
return false, nil, fmt.Errorf("failed to decode 401 response body: %w", err)
|
||||
}
|
||||
if body.Name == "restricted_api_key" {
|
||||
extraData["permission"] = "sending_access"
|
||||
return true, extraData, nil
|
||||
}
|
||||
return false, nil, nil
|
||||
|
||||
case http.StatusBadRequest, http.StatusForbidden:
|
||||
// 400 validation_error: unknown, malformed, or soft-deleted key.
|
||||
// 403 suspended_api_key / restricted_api_key ("not active"):
|
||||
// the key exists but is already disabled; no rotation needed.
|
||||
return false, nil, nil
|
||||
|
||||
default:
|
||||
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Scanner) Type() detector_typepb.DetectorType {
|
||||
return detector_typepb.DetectorType_Resend
|
||||
}
|
||||
|
||||
func (s Scanner) Description() string {
|
||||
return "Resend is an email API for transactional and marketing email. Resend API keys can be used to send email, manage domains and audiences, and access other resources on the account."
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
//go:build detectors
|
||||
// +build detectors
|
||||
|
||||
package resend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
||||
)
|
||||
|
||||
func TestResend_FromChunk(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
secret := testSecrets.MustGetField("RESEND_API_KEY")
|
||||
sendingAccessSecret := testSecrets.MustGetField("RESEND_API_KEY_SENDING_ACCESS")
|
||||
inactiveSecret := testSecrets.MustGetField("RESEND_API_KEY_INACTIVE")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
data []byte
|
||||
verify bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
s Scanner
|
||||
args args
|
||||
want []detectors.Result
|
||||
wantErr bool
|
||||
wantVerificationErr bool
|
||||
}{
|
||||
{
|
||||
name: "found, verified (full_access)",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a resend secret %s within", secret)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_Resend,
|
||||
Verified: true,
|
||||
ExtraData: map[string]string{
|
||||
"permission": "full_access",
|
||||
"rotation_guide": "https://resend.com/docs/dashboard/api-keys/introduction",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, verified (sending_access)",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a resend secret %s within", sendingAccessSecret)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_Resend,
|
||||
Verified: true,
|
||||
ExtraData: map[string]string{
|
||||
"permission": "sending_access",
|
||||
"rotation_guide": "https://resend.com/docs/dashboard/api-keys/introduction",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, unverified",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a resend secret %s within but not valid", inactiveSecret)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_Resend,
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte("You cannot find the secret within"),
|
||||
verify: true,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, would be verified if not for timeout",
|
||||
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a resend secret %s within", secret)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_Resend,
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
{
|
||||
name: "found, verified but unexpected api surface",
|
||||
s: Scanner{client: common.ConstantResponseHttpClient(404, "")},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a resend secret %s within", secret)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_Resend,
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Resend.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if len(got[i].Raw) == 0 {
|
||||
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||
}
|
||||
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
|
||||
t.Errorf("Resend.FromData() error = %v, wantVerificationErr %v", got[i].VerificationError(), tt.wantVerificationErr)
|
||||
}
|
||||
}
|
||||
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "SecretParts")
|
||||
ignoreUnexported := cmpopts.IgnoreUnexported(detectors.Result{})
|
||||
if diff := cmp.Diff(got, tt.want, ignoreOpts, ignoreUnexported); diff != "" {
|
||||
t.Errorf("Resend.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromData(benchmark *testing.B) {
|
||||
ctx := context.Background()
|
||||
s := Scanner{}
|
||||
for name, data := range detectors.MustGetBenchmarkData() {
|
||||
benchmark.Run(name, func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, err := s.FromData(ctx, false, data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package resend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
|
||||
)
|
||||
|
||||
var (
|
||||
validPattern = "re_1234abcd_aBcDeFgHiJkLmNoPqRsTuVwX"
|
||||
invalidPattern = "re_1234abcd_aBcDeFgHiJk0OIlmNoPqRsTu"
|
||||
keyword = "resend"
|
||||
)
|
||||
|
||||
func TestResend_Pattern(t *testing.T) {
|
||||
d := Scanner{}
|
||||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid pattern - with keyword resend",
|
||||
input: fmt.Sprintf("%s token = '%s'", keyword, validPattern),
|
||||
want: []string{validPattern},
|
||||
},
|
||||
{
|
||||
name: "valid pattern - ignore duplicate",
|
||||
input: fmt.Sprintf("%s token = '%s' | '%s'", keyword, validPattern, validPattern),
|
||||
want: []string{validPattern},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - contains disallowed base58 chars (0/O/I/l)",
|
||||
input: fmt.Sprintf("%s = '%s'", keyword, invalidPattern),
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - wrong length",
|
||||
input: fmt.Sprintf("%s = 're_1234abcd_short'", keyword),
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
|
||||
if len(matchedDetectors) == 0 {
|
||||
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
|
||||
return
|
||||
}
|
||||
|
||||
results, err := d.FromData(context.Background(), false, []byte(test.input))
|
||||
if err != nil {
|
||||
t.Errorf("error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) != len(test.want) {
|
||||
if len(results) == 0 {
|
||||
t.Errorf("did not receive result")
|
||||
} else {
|
||||
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
actual := make(map[string]struct{}, len(results))
|
||||
for _, r := range results {
|
||||
if len(r.RawV2) > 0 {
|
||||
actual[string(r.RawV2)] = struct{}{}
|
||||
} else {
|
||||
actual[string(r.Raw)] = struct{}{}
|
||||
}
|
||||
}
|
||||
expected := make(map[string]struct{}, len(test.want))
|
||||
for _, v := range test.want {
|
||||
expected[v] = struct{}{}
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ func (s Scanner) Description() string {
|
||||
func verifyTwilio(ctx context.Context, client *http.Client, key, sid string) (map[string]string, bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://verify.twilio.com/v2/Services", nil)
|
||||
if err != nil {
|
||||
return nil, false, nil
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
@@ -114,7 +114,7 @@ func verifyTwilio(ctx context.Context, client *http.Client, key, sid string) (ma
|
||||
req.SetBasicAuth(sid, key)
|
||||
resp, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_Twilio, sid+key, req)
|
||||
if err != nil {
|
||||
return nil, false, nil
|
||||
return nil, false, err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
|
||||
@@ -2,11 +2,19 @@ package twilio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
|
||||
)
|
||||
@@ -81,3 +89,118 @@ func TestTwilio_Pattern(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type errorTransport struct{ err error }
|
||||
|
||||
func (t errorTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, t.err }
|
||||
|
||||
func TestTwilio_VerificationDeterminacy(t *testing.T) {
|
||||
data := []byte(fmt.Sprintf("twilio sid %s key %s", validSid, validKey))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
client *http.Client
|
||||
wantVerified bool
|
||||
wantVerificationErr bool
|
||||
}{
|
||||
{
|
||||
name: "authenticated rejection is determinate",
|
||||
client: common.ConstantResponseHttpClient(http.StatusUnauthorized, ""),
|
||||
wantVerified: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "success is determinate",
|
||||
client: common.ConstantResponseHttpClient(http.StatusOK, `{"services":[{"friendly_name":"n","sid":"s","account_sid":"a"}]}`),
|
||||
wantVerified: true,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "connection reset is indeterminate",
|
||||
client: &http.Client{Transport: errorTransport{err: errors.New("read: connection reset by peer")}},
|
||||
wantVerified: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
{
|
||||
name: "timeout is indeterminate",
|
||||
client: &http.Client{Transport: errorTransport{err: context.DeadlineExceeded}},
|
||||
wantVerified: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
{
|
||||
name: "rate limit reaching the switch is indeterminate",
|
||||
client: common.ConstantResponseHttpClient(http.StatusTooManyRequests, ""),
|
||||
wantVerified: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
{
|
||||
name: "server error is indeterminate",
|
||||
client: common.ConstantResponseHttpClient(http.StatusInternalServerError, "{}"),
|
||||
wantVerified: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
results, err := Scanner{client: test.client}.FromData(context.Background(), true, data)
|
||||
if err != nil {
|
||||
t.Fatalf("FromData() error = %v", err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("expected 1 result, got %d", len(results))
|
||||
}
|
||||
|
||||
got := results[0]
|
||||
if got.Verified != test.wantVerified {
|
||||
t.Errorf("Verified = %v, want %v", got.Verified, test.wantVerified)
|
||||
}
|
||||
if (got.VerificationError() != nil) != test.wantVerificationErr {
|
||||
t.Errorf("VerificationError() = %v, want error presence %v",
|
||||
got.VerificationError(), test.wantVerificationErr)
|
||||
}
|
||||
if e := got.VerificationError(); e != nil && strings.Contains(e.Error(), validKey) {
|
||||
t.Errorf("verification error leaks the credential: %v", e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwilio_RetryExhaustionIsIndeterminate(t *testing.T) {
|
||||
var attempts int32
|
||||
|
||||
retryClient := retryablehttp.NewClient()
|
||||
retryClient.RetryMax = 3
|
||||
retryClient.Logger = nil
|
||||
retryClient.RetryWaitMin = time.Millisecond
|
||||
retryClient.RetryWaitMax = 2 * time.Millisecond
|
||||
retryClient.HTTPClient.Transport = common.FakeTransport{
|
||||
CreateResponse: func(req *http.Request) (*http.Response, error) {
|
||||
atomic.AddInt32(&attempts, 1)
|
||||
return &http.Response{
|
||||
Request: req,
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
data := []byte(fmt.Sprintf("twilio sid %s key %s", validSid, validKey))
|
||||
results, err := Scanner{client: retryClient.StandardClient()}.FromData(context.Background(), true, data)
|
||||
if err != nil {
|
||||
t.Fatalf("FromData() error = %v", err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("expected 1 result, got %d", len(results))
|
||||
}
|
||||
|
||||
if got := atomic.LoadInt32(&attempts); got != 4 {
|
||||
t.Errorf("expected 4 attempts (initial + RetryMax), got %d", got)
|
||||
}
|
||||
if results[0].Verified {
|
||||
t.Error("Verified = true, want false")
|
||||
}
|
||||
if results[0].VerificationError() == nil {
|
||||
t.Error("VerificationError() = nil; a throttled credential would be recorded as rotated")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func (s Scanner) Description() string {
|
||||
func verifyTwilioAPIKey(ctx context.Context, client *http.Client, apiKey, secret string) (map[string]string, bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://verify.twilio.com/v2/Services", nil)
|
||||
if err != nil {
|
||||
return nil, false, nil
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
@@ -110,7 +110,7 @@ func verifyTwilioAPIKey(ctx context.Context, client *http.Client, apiKey, secret
|
||||
|
||||
resp, err := detectors.DoWithDedup(client, detector_typepb.DetectorType_TwilioApiKey, apiKey+secret, req)
|
||||
if err != nil {
|
||||
return nil, false, nil
|
||||
return nil, false, err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
|
||||
@@ -2,11 +2,15 @@ package twilioapikey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
|
||||
)
|
||||
@@ -102,3 +106,79 @@ func TestTwilioAPIKey_SecretRedacted(t *testing.T) {
|
||||
t.Errorf("expected redacted secret to be '%s', got '%s'", validSecret[:5]+"...", results[0].Redacted)
|
||||
}
|
||||
}
|
||||
|
||||
type errorTransport struct{ err error }
|
||||
|
||||
func (t errorTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, t.err }
|
||||
|
||||
func TestTwilioAPIKey_VerificationDeterminacy(t *testing.T) {
|
||||
data := []byte(fmt.Sprintf("twilio %s %s", validAPIKey, validSecret))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
client *http.Client
|
||||
wantVerified bool
|
||||
wantVerificationErr bool
|
||||
}{
|
||||
{
|
||||
name: "authenticated rejection is determinate",
|
||||
client: common.ConstantResponseHttpClient(http.StatusUnauthorized, ""),
|
||||
wantVerified: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "success is determinate",
|
||||
client: common.ConstantResponseHttpClient(http.StatusOK, `{"services":[{"friendly_name":"n","sid":"s","account_sid":"a"}]}`),
|
||||
wantVerified: true,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "rate limit is indeterminate",
|
||||
client: common.ConstantResponseHttpClient(http.StatusTooManyRequests, ""),
|
||||
wantVerified: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
{
|
||||
name: "connection reset is indeterminate",
|
||||
client: &http.Client{Transport: errorTransport{err: errors.New("read: connection reset by peer")}},
|
||||
wantVerified: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
{
|
||||
name: "timeout is indeterminate",
|
||||
client: &http.Client{Transport: errorTransport{err: context.DeadlineExceeded}},
|
||||
wantVerified: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
{
|
||||
name: "server error is indeterminate",
|
||||
client: common.ConstantResponseHttpClient(http.StatusInternalServerError, "{}"),
|
||||
wantVerified: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
results, err := Scanner{client: test.client}.FromData(context.Background(), true, data)
|
||||
if err != nil {
|
||||
t.Fatalf("FromData() error = %v", err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("expected 1 result, got %d", len(results))
|
||||
}
|
||||
|
||||
got := results[0]
|
||||
if got.Verified != test.wantVerified {
|
||||
t.Errorf("Verified = %v, want %v", got.Verified, test.wantVerified)
|
||||
}
|
||||
if (got.VerificationError() != nil) != test.wantVerificationErr {
|
||||
t.Errorf("VerificationError() = %v, want error presence %v",
|
||||
got.VerificationError(), test.wantVerificationErr)
|
||||
}
|
||||
if e := got.VerificationError(); e != nil && strings.Contains(e.Error(), validSecret) {
|
||||
t.Errorf("verification error leaks the credential: %v", e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +387,7 @@ import (
|
||||
hubspot_apikey_v2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/hubspot_apikey/v2"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/huggingface"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/humanity"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/humioapitoken"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/hunter"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/hybiscus"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/hypertrack"
|
||||
@@ -644,6 +645,7 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/replicate"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/replyio"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/requestfinance"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/resend"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/restpackhtmltopdfapi"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/restpackscreenshotapi"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/rev"
|
||||
@@ -1299,6 +1301,7 @@ func buildDetectorList() []detectors.Detector {
|
||||
&hubspot_apikey_v2.Scanner{},
|
||||
&huggingface.Scanner{},
|
||||
&humanity.Scanner{},
|
||||
&humioapitoken.Scanner{},
|
||||
&hunter.Scanner{},
|
||||
&hybiscus.Scanner{},
|
||||
&hypertrack.Scanner{},
|
||||
@@ -1565,6 +1568,7 @@ func buildDetectorList() []detectors.Detector {
|
||||
&replicate.Scanner{},
|
||||
&replyio.Scanner{},
|
||||
&requestfinance.Scanner{},
|
||||
&resend.Scanner{},
|
||||
// &restpack.Scanner{},
|
||||
&restpackhtmltopdfapi.Scanner{},
|
||||
&restpackscreenshotapi.Scanner{},
|
||||
@@ -1894,6 +1898,10 @@ func buildDetectorList() []detectors.Detector {
|
||||
return !feature.MSTeamsWebhookV2DetectorEnabled.Load()
|
||||
case *solarwindsobservability.Scanner:
|
||||
return !feature.SolarwindsDetectorEnabled.Load()
|
||||
case *humioapitoken.Scanner:
|
||||
return !feature.HumioAPITokenDetectorEnabled.Load()
|
||||
case *resend.Scanner:
|
||||
return !feature.ResendDetectorEnabled.Load()
|
||||
case *weightsandbiasesv2.Scanner:
|
||||
return !feature.WeightsAndBiasesV2DetectorEnabled.Load()
|
||||
default:
|
||||
|
||||
@@ -151,6 +151,8 @@ var excludedFromDefaultList = map[detector_typepb.DetectorType]struct{}{
|
||||
detector_typepb.DetectorType_NewRelicInsightsQueryKey: {},
|
||||
detector_typepb.DetectorType_NewRelicMobileAppToken: {},
|
||||
detector_typepb.DetectorType_SolarWindsObservability: {},
|
||||
detector_typepb.DetectorType_HumioAPIToken: {},
|
||||
detector_typepb.DetectorType_Resend: {},
|
||||
|
||||
// Reserved / special types.
|
||||
detector_typepb.DetectorType_CustomRegex: {}, // added dynamically via engine config, not via buildDetectorList()
|
||||
|
||||
@@ -54,6 +54,11 @@ func (e *Engine) ScanS3(ctx context.Context, c sources.S3Config) (sources.JobPro
|
||||
connection.Roles = c.Roles
|
||||
}
|
||||
|
||||
connection.IncludePrefixes = c.IncludePrefixes
|
||||
connection.ExcludePrefixes = c.ExcludePrefixes
|
||||
connection.IncludeExtensions = c.IncludeExtensions
|
||||
connection.ExcludeExtensions = c.ExcludeExtensions
|
||||
|
||||
connection.Endpoint = c.Endpoint
|
||||
connection.Region = c.Region
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ var (
|
||||
NewRelicMobileAppTokenDetectorEnabled atomic.Bool
|
||||
MSTeamsWebhookV2DetectorEnabled atomic.Bool
|
||||
SolarwindsDetectorEnabled atomic.Bool
|
||||
ResendDetectorEnabled atomic.Bool
|
||||
WeightsAndBiasesV2DetectorEnabled atomic.Bool
|
||||
HumioAPITokenDetectorEnabled atomic.Bool
|
||||
)
|
||||
|
||||
type AtomicString struct {
|
||||
|
||||
@@ -1125,6 +1125,8 @@ const (
|
||||
DetectorType_NewRelicInsightsQueryKey DetectorType = 1067
|
||||
DetectorType_NewRelicMobileAppToken DetectorType = 1068
|
||||
DetectorType_SolarWindsObservability DetectorType = 1069
|
||||
DetectorType_HumioAPIToken DetectorType = 1070
|
||||
DetectorType_Resend DetectorType = 1071
|
||||
)
|
||||
|
||||
// Enum value maps for DetectorType.
|
||||
@@ -2196,6 +2198,8 @@ var (
|
||||
1067: "NewRelicInsightsQueryKey",
|
||||
1068: "NewRelicMobileAppToken",
|
||||
1069: "SolarWindsObservability",
|
||||
1070: "HumioAPIToken",
|
||||
1071: "Resend",
|
||||
}
|
||||
DetectorType_value = map[string]int32{
|
||||
"Alibaba": 0,
|
||||
@@ -3264,6 +3268,8 @@ var (
|
||||
"NewRelicInsightsQueryKey": 1067,
|
||||
"NewRelicMobileAppToken": 1068,
|
||||
"SolarWindsObservability": 1069,
|
||||
"HumioAPIToken": 1070,
|
||||
"Resend": 1071,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3299,7 +3305,7 @@ var File_detector_type_proto protoreflect.FileDescriptor
|
||||
var file_detector_type_proto_rawDesc = []byte{
|
||||
0x0a, 0x13, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f,
|
||||
0x74, 0x79, 0x70, 0x65, 0x2a, 0xf3, 0x8b, 0x01, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74,
|
||||
0x74, 0x79, 0x70, 0x65, 0x2a, 0x94, 0x8c, 0x01, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74,
|
||||
0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x6c, 0x69, 0x62, 0x61, 0x62,
|
||||
0x61, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x4d, 0x51, 0x50, 0x10, 0x01, 0x12, 0x07, 0x0a,
|
||||
0x03, 0x41, 0x57, 0x53, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x10,
|
||||
@@ -4418,12 +4424,14 @@ var file_detector_type_proto_rawDesc = []byte{
|
||||
0x12, 0x1b, 0x0a, 0x16, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x6c, 0x69, 0x63, 0x4d, 0x6f, 0x62, 0x69,
|
||||
0x6c, 0x65, 0x41, 0x70, 0x70, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xac, 0x08, 0x12, 0x1c, 0x0a,
|
||||
0x17, 0x53, 0x6f, 0x6c, 0x61, 0x72, 0x57, 0x69, 0x6e, 0x64, 0x73, 0x4f, 0x62, 0x73, 0x65, 0x72,
|
||||
0x76, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x10, 0xad, 0x08, 0x42, 0x41, 0x5a, 0x3f, 0x67,
|
||||
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c,
|
||||
0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c,
|
||||
0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64,
|
||||
0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x70, 0x62, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x76, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x10, 0xad, 0x08, 0x12, 0x12, 0x0a, 0x0d, 0x48,
|
||||
0x75, 0x6d, 0x69, 0x6f, 0x41, 0x50, 0x49, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xae, 0x08, 0x12,
|
||||
0x0b, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x65, 0x6e, 0x64, 0x10, 0xaf, 0x08, 0x42, 0x41, 0x5a, 0x3f,
|
||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66,
|
||||
0x6c, 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66,
|
||||
0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f,
|
||||
0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x70, 0x62, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
+394
-341
@@ -3283,6 +3283,20 @@ type S3 struct {
|
||||
Endpoint string `protobuf:"bytes,10,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||
// region is the region used to sign requests. Defaults to us-east-1.
|
||||
Region string `protobuf:"bytes,11,opt,name=region,proto3" json:"region,omitempty"`
|
||||
// include_prefixes limits the scan to objects whose key matches one of these
|
||||
// key prefixes. Leave empty to consider every object in the bucket.
|
||||
IncludePrefixes []string `protobuf:"bytes,12,rep,name=include_prefixes,json=includePrefixes,proto3" json:"include_prefixes,omitempty"`
|
||||
// exclude_prefixes skips objects whose key matches one of these key prefixes.
|
||||
ExcludePrefixes []string `protobuf:"bytes,13,rep,name=exclude_prefixes,json=excludePrefixes,proto3" json:"exclude_prefixes,omitempty"`
|
||||
// include_extensions limits the scan to objects whose key ends in one of these
|
||||
// file extensions, written without a leading dot (e.g. "tf", "yaml"). Leave
|
||||
// empty to consider every extension. Cannot be combined with
|
||||
// exclude_extensions.
|
||||
IncludeExtensions []string `protobuf:"bytes,14,rep,name=include_extensions,json=includeExtensions,proto3" json:"include_extensions,omitempty"`
|
||||
// exclude_extensions skips objects whose key ends in one of these file
|
||||
// extensions, written without a leading dot (e.g. "zip", "mp4"). Cannot be
|
||||
// combined with include_extensions.
|
||||
ExcludeExtensions []string `protobuf:"bytes,15,rep,name=exclude_extensions,json=excludeExtensions,proto3" json:"exclude_extensions,omitempty"`
|
||||
}
|
||||
|
||||
func (x *S3) Reset() {
|
||||
@@ -3402,6 +3416,34 @@ func (x *S3) GetRegion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *S3) GetIncludePrefixes() []string {
|
||||
if x != nil {
|
||||
return x.IncludePrefixes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *S3) GetExcludePrefixes() []string {
|
||||
if x != nil {
|
||||
return x.ExcludePrefixes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *S3) GetIncludeExtensions() []string {
|
||||
if x != nil {
|
||||
return x.IncludeExtensions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *S3) GetExcludeExtensions() []string {
|
||||
if x != nil {
|
||||
return x.ExcludeExtensions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isS3_Credential interface {
|
||||
isS3_Credential()
|
||||
}
|
||||
@@ -5755,7 +5797,7 @@ var file_sources_proto_rawDesc = []byte{
|
||||
0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x55, 0x6e, 0x61,
|
||||
0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0f,
|
||||
0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x42,
|
||||
0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0x9c, 0x04,
|
||||
0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xd0, 0x05,
|
||||
0x0a, 0x02, 0x53, 0x33, 0x12, 0x37, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b,
|
||||
0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
|
||||
@@ -5788,348 +5830,359 @@ var file_sources_proto_rawDesc = []byte{
|
||||
0x12, 0x24, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01,
|
||||
0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x90, 0x01, 0x01, 0x52, 0x08, 0x65, 0x6e,
|
||||
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e,
|
||||
0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x42, 0x0c,
|
||||
0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xc4, 0x01, 0x0a,
|
||||
0x05, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x24, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69,
|
||||
0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x29,
|
||||
0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78,
|
||||
0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64,
|
||||
0x65, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x78, 0x63,
|
||||
0x6c, 0x75, 0x64, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x18, 0x0d, 0x20,
|
||||
0x03, 0x28, 0x09, 0x52, 0x0f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x72, 0x65, 0x66,
|
||||
0x69, 0x78, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f,
|
||||
0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x11, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x65,
|
||||
0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x09, 0x52,
|
||||
0x11, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c,
|
||||
0x22, 0xc4, 0x01, 0x0a, 0x05, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x12, 0x24, 0x0a, 0x08, 0x65, 0x6e,
|
||||
0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42,
|
||||
0x05, 0x72, 0x03, 0x90, 0x01, 0x01, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
|
||||
0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48,
|
||||
0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x32, 0x0a, 0x06, 0x74, 0x6f, 0x6b, 0x65,
|
||||
0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65,
|
||||
0x6e, 0x73, 0x48, 0x00, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1a, 0x0a, 0x08,
|
||||
0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08,
|
||||
0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x67, 0x6e, 0x6f,
|
||||
0x72, 0x65, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x69,
|
||||
0x67, 0x6e, 0x6f, 0x72, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65,
|
||||
0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0x06, 0x0a, 0x04, 0x54, 0x65, 0x73, 0x74, 0x22,
|
||||
0x31, 0x0a, 0x09, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x6b, 0x69, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x05,
|
||||
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74,
|
||||
0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69,
|
||||
0x61, 0x6c, 0x22, 0xa5, 0x02, 0x0a, 0x06, 0x47, 0x65, 0x72, 0x72, 0x69, 0x74, 0x12, 0x24, 0x0a,
|
||||
0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42,
|
||||
0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x90, 0x01, 0x01, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f,
|
||||
0x69, 0x6e, 0x74, 0x12, 0x37, 0x0a, 0x0a, 0x62, 0x61, 0x73, 0x69, 0x63, 0x5f, 0x61, 0x75, 0x74,
|
||||
0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x42, 0x61, 0x73, 0x69, 0x63, 0x41, 0x75, 0x74, 0x68, 0x48,
|
||||
0x00, 0x52, 0x09, 0x62, 0x61, 0x73, 0x69, 0x63, 0x41, 0x75, 0x74, 0x68, 0x12, 0x48, 0x0a, 0x0f,
|
||||
0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18,
|
||||
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69,
|
||||
0x61, 0x6c, 0x73, 0x2e, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61,
|
||||
0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0f, 0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74,
|
||||
0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63,
|
||||
0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63,
|
||||
0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x62, 0x69, 0x6e, 0x61, 0x72,
|
||||
0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73, 0x6b, 0x69, 0x70, 0x42,
|
||||
0x69, 0x6e, 0x61, 0x72, 0x69, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x5f,
|
||||
0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c,
|
||||
0x73, 0x6b, 0x69, 0x70, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x73, 0x42, 0x0c, 0x0a, 0x0a,
|
||||
0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xa8, 0x02, 0x0a, 0x07, 0x4a,
|
||||
0x65, 0x6e, 0x6b, 0x69, 0x6e, 0x73, 0x12, 0x24, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69,
|
||||
0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x90,
|
||||
0x01, 0x01, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x05,
|
||||
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74,
|
||||
0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x32, 0x0a, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x05,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61,
|
||||
0x6c, 0x73, 0x2e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x48, 0x00,
|
||||
0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x6e,
|
||||
0x6e, 0x65, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x63, 0x68, 0x61, 0x6e,
|
||||
0x6e, 0x65, 0x6c, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x5f, 0x6c,
|
||||
0x69, 0x73, 0x74, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x67, 0x6e, 0x6f, 0x72,
|
||||
0x65, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74,
|
||||
0x69, 0x61, 0x6c, 0x22, 0x06, 0x0a, 0x04, 0x54, 0x65, 0x73, 0x74, 0x22, 0x31, 0x0a, 0x09, 0x42,
|
||||
0x75, 0x69, 0x6c, 0x64, 0x6b, 0x69, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65,
|
||||
0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
|
||||
0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xa5,
|
||||
0x02, 0x0a, 0x06, 0x47, 0x65, 0x72, 0x72, 0x69, 0x74, 0x12, 0x24, 0x0a, 0x08, 0x65, 0x6e, 0x64,
|
||||
0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05,
|
||||
0x72, 0x03, 0x90, 0x01, 0x01, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
|
||||
0x37, 0x0a, 0x0a, 0x62, 0x61, 0x73, 0x69, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c,
|
||||
0x73, 0x2e, 0x42, 0x61, 0x73, 0x69, 0x63, 0x41, 0x75, 0x74, 0x68, 0x48, 0x00, 0x52, 0x09, 0x62,
|
||||
0x61, 0x73, 0x69, 0x63, 0x41, 0x75, 0x74, 0x68, 0x12, 0x48, 0x0a, 0x0f, 0x75, 0x6e, 0x61, 0x75,
|
||||
0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e,
|
||||
0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x48,
|
||||
0x00, 0x52, 0x0f, 0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74,
|
||||
0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x04,
|
||||
0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x23,
|
||||
0x0a, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x62, 0x69, 0x6e, 0x61, 0x72, 0x69, 0x65, 0x73, 0x18,
|
||||
0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73, 0x6b, 0x69, 0x70, 0x42, 0x69, 0x6e, 0x61, 0x72,
|
||||
0x69, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x61, 0x72, 0x63, 0x68,
|
||||
0x69, 0x76, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73, 0x6b, 0x69, 0x70,
|
||||
0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64,
|
||||
0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xa8, 0x02, 0x0a, 0x07, 0x4a, 0x65, 0x6e, 0x6b, 0x69,
|
||||
0x6e, 0x73, 0x12, 0x24, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x90, 0x01, 0x01, 0x52, 0x08,
|
||||
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x37, 0x0a, 0x0a, 0x62, 0x61, 0x73, 0x69,
|
||||
0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63,
|
||||
0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x42, 0x61, 0x73, 0x69, 0x63,
|
||||
0x41, 0x75, 0x74, 0x68, 0x48, 0x00, 0x52, 0x09, 0x62, 0x61, 0x73, 0x69, 0x63, 0x41, 0x75, 0x74,
|
||||
0x68, 0x12, 0x2d, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e,
|
||||
0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x48, 0x00, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72,
|
||||
0x12, 0x48, 0x0a, 0x0f, 0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61,
|
||||
0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x72, 0x65, 0x64,
|
||||
0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0f, 0x75, 0x6e, 0x61, 0x75, 0x74,
|
||||
0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x37, 0x0a, 0x18, 0x69, 0x6e,
|
||||
0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x5f, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x76, 0x65, 0x72, 0x69,
|
||||
0x66, 0x79, 0x5f, 0x74, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x69, 0x6e,
|
||||
0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79,
|
||||
0x54, 0x6c, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61,
|
||||
0x6c, 0x22, 0xbd, 0x02, 0x0a, 0x05, 0x54, 0x65, 0x61, 0x6d, 0x73, 0x12, 0x24, 0x0a, 0x08, 0x65,
|
||||
0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa,
|
||||
0x42, 0x05, 0x72, 0x03, 0x90, 0x01, 0x01, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
|
||||
0x74, 0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||
0x48, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x46, 0x0a, 0x0d, 0x61, 0x75, 0x74,
|
||||
0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x1e, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x43,
|
||||
0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73,
|
||||
0x48, 0x00, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65,
|
||||
0x64, 0x12, 0x2b, 0x0a, 0x05, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x13, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x4f,
|
||||
0x61, 0x75, 0x74, 0x68, 0x32, 0x48, 0x00, 0x52, 0x05, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x12, 0x1a,
|
||||
0x0a, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x67,
|
||||
0x6e, 0x6f, 0x72, 0x65, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52,
|
||||
0x0a, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x74,
|
||||
0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x74,
|
||||
0x65, 0x61, 0x6d, 0x49, 0x64, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74,
|
||||
0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e,
|
||||
0x74, 0x49, 0x64, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61,
|
||||
0x6c, 0x22, 0x95, 0x01, 0x0a, 0x06, 0x53, 0x79, 0x73, 0x6c, 0x6f, 0x67, 0x12, 0x1a, 0x0a, 0x08,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x6c, 0x69, 0x73, 0x74,
|
||||
0x65, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0d, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12,
|
||||
0x18, 0x0a, 0x07, 0x74, 0x6c, 0x73, 0x43, 0x65, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x07, 0x74, 0x6c, 0x73, 0x43, 0x65, 0x72, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x6c, 0x73,
|
||||
0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x6c, 0x73, 0x4b, 0x65,
|
||||
0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0xca, 0x01, 0x0a, 0x07, 0x46, 0x6f,
|
||||
0x72, 0x61, 0x67, 0x65, 0x72, 0x12, 0x48, 0x0a, 0x0f, 0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c,
|
||||
0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x55, 0x6e, 0x61,
|
||||
0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0f,
|
||||
0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12,
|
||||
0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78,
|
||||
0x5f, 0x64, 0x65, 0x70, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6d, 0x61,
|
||||
0x78, 0x44, 0x65, 0x70, 0x74, 0x68, 0x12, 0x30, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||
0x70, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64,
|
||||
0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0x51, 0x0a, 0x0d, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x52,
|
||||
0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
|
||||
0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
|
||||
0x73, 0x48, 0x00, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x63,
|
||||
0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xe9, 0x01, 0x0a, 0x0a, 0x53, 0x68,
|
||||
0x61, 0x72, 0x65, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x2b, 0x0a, 0x05, 0x6f, 0x61, 0x75, 0x74,
|
||||
0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x48, 0x00, 0x52, 0x05,
|
||||
0x6f, 0x61, 0x75, 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x75, 0x72,
|
||||
0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x69, 0x74, 0x65, 0x55, 0x72, 0x6c,
|
||||
0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c,
|
||||
0x75, 0x64, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x69,
|
||||
0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x43, 0x6f,
|
||||
0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2f, 0x0a, 0x13, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64,
|
||||
0x65, 0x5f, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20,
|
||||
0x01, 0x28, 0x08, 0x52, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x41, 0x74, 0x74, 0x61,
|
||||
0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xf6, 0x03, 0x0a, 0x0a, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x52,
|
||||
0x65, 0x70, 0x6f, 0x73, 0x12, 0x24, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x90, 0x01, 0x01,
|
||||
0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f,
|
||||
0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b,
|
||||
0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e,
|
||||
0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x48, 0x00, 0x52, 0x05, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x12,
|
||||
0x22, 0x0a, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x18,
|
||||
0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72,
|
||||
0x69, 0x65, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x6f, 0x72, 0x67, 0x61,
|
||||
0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f,
|
||||
0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f,
|
||||
0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65,
|
||||
0x5f, 0x66, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e,
|
||||
0x63, 0x6c, 0x75, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x67,
|
||||
0x6e, 0x6f, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x0b, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x12, 0x23, 0x0a,
|
||||
0x0d, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x18, 0x09,
|
||||
0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x52, 0x65, 0x70,
|
||||
0x6f, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x70, 0x72,
|
||||
0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x69, 0x6e,
|
||||
0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x27, 0x0a,
|
||||
0x0f, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x5f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73,
|
||||
0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x50, 0x72,
|
||||
0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x62,
|
||||
0x69, 0x6e, 0x61, 0x72, 0x69, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73,
|
||||
0x6b, 0x69, 0x70, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x69, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73,
|
||||
0x6b, 0x69, 0x70, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x73, 0x18, 0x0d, 0x20, 0x01,
|
||||
0x28, 0x08, 0x52, 0x0c, 0x73, 0x6b, 0x69, 0x70, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x73,
|
||||
0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xd5,
|
||||
0x04, 0x0a, 0x07, 0x50, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x6e, 0x12, 0x48, 0x0a, 0x0f, 0x75, 0x6e,
|
||||
0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c,
|
||||
0x73, 0x2e, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65,
|
||||
0x64, 0x48, 0x00, 0x52, 0x0f, 0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63,
|
||||
0x61, 0x74, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e, 0x0a, 0x0a,
|
||||
0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x0a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b,
|
||||
0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28,
|
||||
0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22,
|
||||
0x0a, 0x0c, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x05,
|
||||
0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e,
|
||||
0x74, 0x73, 0x12, 0x2f, 0x0a, 0x13, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x63, 0x6f,
|
||||
0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52,
|
||||
0x12, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x73, 0x12, 0x31, 0x0a, 0x14, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x65,
|
||||
0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
|
||||
0x09, 0x52, 0x13, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f,
|
||||
0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2f, 0x0a, 0x13, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64,
|
||||
0x65, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20,
|
||||
0x03, 0x28, 0x09, 0x52, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x43, 0x6f, 0x6c, 0x6c,
|
||||
0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x31, 0x0a, 0x14, 0x69, 0x6e, 0x63, 0x6c, 0x75,
|
||||
0x01, 0x01, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x37, 0x0a, 0x0a,
|
||||
0x62, 0x61, 0x73, 0x69, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x16, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x42,
|
||||
0x61, 0x73, 0x69, 0x63, 0x41, 0x75, 0x74, 0x68, 0x48, 0x00, 0x52, 0x09, 0x62, 0x61, 0x73, 0x69,
|
||||
0x63, 0x41, 0x75, 0x74, 0x68, 0x12, 0x2d, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18,
|
||||
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69,
|
||||
0x61, 0x6c, 0x73, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x48, 0x00, 0x52, 0x06, 0x68, 0x65,
|
||||
0x61, 0x64, 0x65, 0x72, 0x12, 0x48, 0x0a, 0x0f, 0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e,
|
||||
0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x55, 0x6e, 0x61, 0x75,
|
||||
0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0f, 0x75,
|
||||
0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x37,
|
||||
0x0a, 0x18, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x5f, 0x73, 0x6b, 0x69, 0x70, 0x5f,
|
||||
0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x74, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08,
|
||||
0x52, 0x15, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, 0x70, 0x56, 0x65,
|
||||
0x72, 0x69, 0x66, 0x79, 0x54, 0x6c, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xbd, 0x02, 0x0a, 0x05, 0x54, 0x65, 0x61, 0x6d, 0x73, 0x12,
|
||||
0x24, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x90, 0x01, 0x01, 0x52, 0x08, 0x65, 0x6e, 0x64,
|
||||
0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x46, 0x0a,
|
||||
0x0d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61,
|
||||
0x6c, 0x73, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74,
|
||||
0x69, 0x61, 0x6c, 0x73, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69,
|
||||
0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x2b, 0x0a, 0x05, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x18, 0x07,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61,
|
||||
0x6c, 0x73, 0x2e, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x48, 0x00, 0x52, 0x05, 0x6f, 0x61, 0x75,
|
||||
0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x18, 0x04,
|
||||
0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x12, 0x1f,
|
||||
0x0a, 0x0b, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x05, 0x20,
|
||||
0x03, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12,
|
||||
0x19, 0x0a, 0x08, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28,
|
||||
0x09, 0x52, 0x07, 0x74, 0x65, 0x61, 0x6d, 0x49, 0x64, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x65,
|
||||
0x6e, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74,
|
||||
0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0x95, 0x01, 0x0a, 0x06, 0x53, 0x79, 0x73, 0x6c, 0x6f, 0x67,
|
||||
0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x25, 0x0a, 0x0e,
|
||||
0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x41, 0x64, 0x64, 0x72,
|
||||
0x65, 0x73, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x6c, 0x73, 0x43, 0x65, 0x72, 0x74, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x6c, 0x73, 0x43, 0x65, 0x72, 0x74, 0x12, 0x16, 0x0a,
|
||||
0x06, 0x74, 0x6c, 0x73, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74,
|
||||
0x6c, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18,
|
||||
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0xca, 0x01,
|
||||
0x0a, 0x07, 0x46, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x72, 0x12, 0x48, 0x0a, 0x0f, 0x75, 0x6e, 0x61,
|
||||
0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73,
|
||||
0x2e, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64,
|
||||
0x48, 0x00, 0x52, 0x0f, 0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61,
|
||||
0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02,
|
||||
0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1b, 0x0a,
|
||||
0x09, 0x6d, 0x61, 0x78, 0x5f, 0x64, 0x65, 0x70, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x08, 0x6d, 0x61, 0x78, 0x44, 0x65, 0x70, 0x74, 0x68, 0x12, 0x30, 0x0a, 0x05, 0x73, 0x69,
|
||||
0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
|
||||
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x42, 0x0c, 0x0a, 0x0a,
|
||||
0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0x51, 0x0a, 0x0d, 0x53, 0x6c,
|
||||
0x61, 0x63, 0x6b, 0x52, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x74,
|
||||
0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x72,
|
||||
0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x54,
|
||||
0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x06, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42,
|
||||
0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xe9, 0x01,
|
||||
0x0a, 0x0a, 0x53, 0x68, 0x61, 0x72, 0x65, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x2b, 0x0a, 0x05,
|
||||
0x6f, 0x61, 0x75, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72,
|
||||
0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32,
|
||||
0x48, 0x00, 0x52, 0x05, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, 0x74,
|
||||
0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x69, 0x74,
|
||||
0x65, 0x55, 0x72, 0x6c, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f,
|
||||
0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f,
|
||||
0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12,
|
||||
0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x65,
|
||||
0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75,
|
||||
0x64, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2f, 0x0a, 0x13, 0x69, 0x6e,
|
||||
0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74,
|
||||
0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65,
|
||||
0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x63,
|
||||
0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xf6, 0x03, 0x0a, 0x0a, 0x41, 0x7a,
|
||||
0x75, 0x72, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x12, 0x24, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70,
|
||||
0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72,
|
||||
0x03, 0x90, 0x01, 0x01, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x16,
|
||||
0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52,
|
||||
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x18,
|
||||
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69,
|
||||
0x61, 0x6c, 0x73, 0x2e, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x48, 0x00, 0x52, 0x05, 0x6f, 0x61,
|
||||
0x75, 0x74, 0x68, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72,
|
||||
0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6f, 0x73,
|
||||
0x69, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x6f, 0x72, 0x67, 0x61, 0x6e,
|
||||
0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d,
|
||||
0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x0a,
|
||||
0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52,
|
||||
0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x63,
|
||||
0x6c, 0x75, 0x64, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08,
|
||||
0x52, 0x0c, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x21,
|
||||
0x0a, 0x0c, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x18, 0x08,
|
||||
0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x70, 0x6f,
|
||||
0x73, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x72, 0x65, 0x70,
|
||||
0x6f, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64,
|
||||
0x65, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64,
|
||||
0x65, 0x5f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74,
|
||||
0x73, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x5f, 0x70, 0x72, 0x6f, 0x6a,
|
||||
0x65, 0x63, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x67, 0x6e, 0x6f,
|
||||
0x72, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x6b,
|
||||
0x69, 0x70, 0x5f, 0x62, 0x69, 0x6e, 0x61, 0x72, 0x69, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28,
|
||||
0x08, 0x52, 0x0c, 0x73, 0x6b, 0x69, 0x70, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x69, 0x65, 0x73, 0x12,
|
||||
0x23, 0x0a, 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x73,
|
||||
0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73, 0x6b, 0x69, 0x70, 0x41, 0x72, 0x63, 0x68,
|
||||
0x69, 0x76, 0x65, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69,
|
||||
0x61, 0x6c, 0x22, 0xd5, 0x04, 0x0a, 0x07, 0x50, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x6e, 0x12, 0x48,
|
||||
0x0a, 0x0f, 0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65,
|
||||
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69,
|
||||
0x63, 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0f, 0x75, 0x6e, 0x61, 0x75, 0x74, 0x68, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65,
|
||||
0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
|
||||
0x12, 0x1e, 0x0a, 0x0a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x03,
|
||||
0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73,
|
||||
0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18,
|
||||
0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e,
|
||||
0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f,
|
||||
0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2f, 0x0a, 0x13, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64,
|
||||
0x65, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20,
|
||||
0x03, 0x28, 0x09, 0x52, 0x12, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x43, 0x6f, 0x6c, 0x6c,
|
||||
0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x31, 0x0a, 0x14, 0x65, 0x78, 0x63, 0x6c, 0x75,
|
||||
0x64, 0x65, 0x5f, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18,
|
||||
0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x45, 0x6e,
|
||||
0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x65,
|
||||
0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x18,
|
||||
0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x4b,
|
||||
0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73,
|
||||
0x70, 0x61, 0x63, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73,
|
||||
0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70,
|
||||
0x61, 0x74, 0x68, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6c, 0x6c,
|
||||
0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x65,
|
||||
0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73,
|
||||
0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d,
|
||||
0x65, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64,
|
||||
0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xac, 0x01, 0x0a, 0x07, 0x57, 0x65, 0x62, 0x68, 0x6f,
|
||||
0x6f, 0x6b, 0x12, 0x2e, 0x0a, 0x0e, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x61, 0x64, 0x64,
|
||||
0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72,
|
||||
0x02, 0x68, 0x01, 0x52, 0x0d, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65,
|
||||
0x73, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73,
|
||||
0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x48, 0x00, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65,
|
||||
0x72, 0x12, 0x29, 0x0a, 0x06, 0x76, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x0f, 0x2e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x56, 0x65, 0x63, 0x74,
|
||||
0x6f, 0x72, 0x48, 0x01, 0x52, 0x06, 0x76, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x0c, 0x0a, 0x0a,
|
||||
0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x42, 0x09, 0x0a, 0x07, 0x76, 0x61,
|
||||
0x72, 0x69, 0x61, 0x6e, 0x74, 0x22, 0x4e, 0x0a, 0x06, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12,
|
||||
0x23, 0x0a, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x46,
|
||||
0x69, 0x65, 0x6c, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x6e, 0x6b, 0x5f, 0x66, 0x6f, 0x72,
|
||||
0x6d, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x69, 0x6e, 0x6b, 0x46,
|
||||
0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0xcd, 0x02, 0x0a, 0x0d, 0x45, 0x6c, 0x61, 0x73, 0x74, 0x69,
|
||||
0x63, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73,
|
||||
0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x1a, 0x0a,
|
||||
0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73,
|
||||
0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73,
|
||||
0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x49, 0x64,
|
||||
0x12, 0x17, 0x0a, 0x07, 0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x06, 0x61, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x72,
|
||||
0x76, 0x69, 0x63, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x23,
|
||||
0x0a, 0x0d, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18,
|
||||
0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x50, 0x61, 0x74, 0x74,
|
||||
0x65, 0x72, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x6a, 0x73, 0x6f,
|
||||
0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, 0x4a, 0x73,
|
||||
0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65,
|
||||
0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x69, 0x6e,
|
||||
0x63, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x28, 0x0a, 0x10, 0x62,
|
||||
0x65, 0x73, 0x74, 0x5f, 0x65, 0x66, 0x66, 0x6f, 0x72, 0x74, 0x5f, 0x73, 0x63, 0x61, 0x6e, 0x18,
|
||||
0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x62, 0x65, 0x73, 0x74, 0x45, 0x66, 0x66, 0x6f, 0x72,
|
||||
0x74, 0x53, 0x63, 0x61, 0x6e, 0x22, 0xde, 0x01, 0x0a, 0x06, 0x53, 0x65, 0x6e, 0x74, 0x72, 0x79,
|
||||
0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0a,
|
||||
0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||
0x48, 0x00, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x19, 0x0a,
|
||||
0x07, 0x64, 0x73, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00,
|
||||
0x52, 0x06, 0x64, 0x73, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x0a, 0x07, 0x61, 0x70, 0x69, 0x5f,
|
||||
0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x61, 0x70, 0x69,
|
||||
0x4b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x18, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x5f,
|
||||
0x73, 0x6b, 0x69, 0x70, 0x5f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x74, 0x6c, 0x73, 0x18,
|
||||
0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53,
|
||||
0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x54, 0x6c, 0x73, 0x12, 0x1a, 0x0a, 0x08,
|
||||
0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
|
||||
0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64,
|
||||
0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0x07, 0x0a, 0x05, 0x53, 0x74, 0x64, 0x69, 0x6e, 0x22,
|
||||
0x4e, 0x0a, 0x0f, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x6f,
|
||||
0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
|
||||
0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x22,
|
||||
0x26, 0x0a, 0x0e, 0x4a, 0x53, 0x4f, 0x4e, 0x45, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x61, 0x74, 0x6f,
|
||||
0x72, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x2a, 0xe9, 0x09, 0x0a, 0x0a, 0x53, 0x6f, 0x75, 0x72,
|
||||
0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45,
|
||||
0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x5a, 0x55, 0x52, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x52,
|
||||
0x41, 0x47, 0x45, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f,
|
||||
0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x49, 0x54, 0x42, 0x55, 0x43, 0x4b, 0x45, 0x54, 0x10, 0x01,
|
||||
0x12, 0x18, 0x0a, 0x14, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f,
|
||||
0x43, 0x49, 0x52, 0x43, 0x4c, 0x45, 0x43, 0x49, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x53, 0x4f,
|
||||
0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x4c, 0x55,
|
||||
0x45, 0x4e, 0x43, 0x45, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45,
|
||||
0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x43, 0x4b, 0x45, 0x52, 0x10, 0x04, 0x12, 0x13,
|
||||
0x0a, 0x0f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x43,
|
||||
0x52, 0x10, 0x05, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59,
|
||||
0x50, 0x45, 0x5f, 0x47, 0x43, 0x53, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x4f, 0x55, 0x52,
|
||||
0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x48, 0x55, 0x42, 0x10, 0x07,
|
||||
0x12, 0x1a, 0x0a, 0x16, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f,
|
||||
0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x47, 0x49, 0x54, 0x10, 0x08, 0x12, 0x16, 0x0a, 0x12,
|
||||
0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x4c,
|
||||
0x41, 0x42, 0x10, 0x09, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54,
|
||||
0x59, 0x50, 0x45, 0x5f, 0x4a, 0x49, 0x52, 0x41, 0x10, 0x0a, 0x12, 0x24, 0x0a, 0x20, 0x53, 0x4f,
|
||||
0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x50, 0x4d, 0x5f, 0x55, 0x4e,
|
||||
0x41, 0x55, 0x54, 0x48, 0x44, 0x5f, 0x50, 0x41, 0x43, 0x4b, 0x41, 0x47, 0x45, 0x53, 0x10, 0x0b,
|
||||
0x12, 0x25, 0x0a, 0x21, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f,
|
||||
0x50, 0x59, 0x50, 0x49, 0x5f, 0x55, 0x4e, 0x41, 0x55, 0x54, 0x48, 0x44, 0x5f, 0x50, 0x41, 0x43,
|
||||
0x4b, 0x41, 0x47, 0x45, 0x53, 0x10, 0x0c, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x4f, 0x55, 0x52, 0x43,
|
||||
0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x33, 0x10, 0x0d, 0x12, 0x15, 0x0a, 0x11, 0x53,
|
||||
0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x4c, 0x41, 0x43, 0x4b,
|
||||
0x10, 0x0e, 0x12, 0x1a, 0x0a, 0x16, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50,
|
||||
0x45, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x10, 0x0f, 0x12, 0x13,
|
||||
0x0a, 0x0f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49,
|
||||
0x54, 0x10, 0x10, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59,
|
||||
0x50, 0x45, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x10, 0x11, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x4f, 0x55,
|
||||
0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x33, 0x5f, 0x55, 0x4e, 0x41, 0x55,
|
||||
0x54, 0x48, 0x45, 0x44, 0x10, 0x12, 0x12, 0x2a, 0x0a, 0x26, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45,
|
||||
0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x48, 0x55, 0x42, 0x5f, 0x55, 0x4e, 0x41,
|
||||
0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x4f, 0x52, 0x47,
|
||||
0x10, 0x13, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50,
|
||||
0x45, 0x5f, 0x42, 0x55, 0x49, 0x4c, 0x44, 0x4b, 0x49, 0x54, 0x45, 0x10, 0x14, 0x12, 0x16, 0x0a,
|
||||
0x12, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x45, 0x52,
|
||||
0x52, 0x49, 0x54, 0x10, 0x15, 0x12, 0x17, 0x0a, 0x13, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f,
|
||||
0x54, 0x59, 0x50, 0x45, 0x5f, 0x4a, 0x45, 0x4e, 0x4b, 0x49, 0x4e, 0x53, 0x10, 0x16, 0x12, 0x15,
|
||||
0x0a, 0x11, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x54, 0x45,
|
||||
0x41, 0x4d, 0x53, 0x10, 0x17, 0x12, 0x21, 0x0a, 0x1d, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f,
|
||||
0x54, 0x59, 0x50, 0x45, 0x5f, 0x4a, 0x46, 0x52, 0x4f, 0x47, 0x5f, 0x41, 0x52, 0x54, 0x49, 0x46,
|
||||
0x41, 0x43, 0x54, 0x4f, 0x52, 0x59, 0x10, 0x18, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x4f, 0x55, 0x52,
|
||||
0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x59, 0x53, 0x4c, 0x4f, 0x47, 0x10, 0x19,
|
||||
0x12, 0x27, 0x0a, 0x23, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f,
|
||||
0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x4d, 0x4f, 0x4e,
|
||||
0x49, 0x54, 0x4f, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x1a, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4f, 0x55,
|
||||
0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x4c, 0x41, 0x43, 0x4b, 0x5f, 0x52,
|
||||
0x45, 0x41, 0x4c, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x1b, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x4f, 0x55,
|
||||
0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x4f, 0x4f, 0x47, 0x4c, 0x45, 0x5f,
|
||||
0x44, 0x52, 0x49, 0x56, 0x45, 0x10, 0x1c, 0x12, 0x1a, 0x0a, 0x16, 0x53, 0x4f, 0x55, 0x52, 0x43,
|
||||
0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x48, 0x41, 0x52, 0x45, 0x50, 0x4f, 0x49, 0x4e,
|
||||
0x54, 0x10, 0x1d, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59,
|
||||
0x50, 0x45, 0x5f, 0x47, 0x43, 0x53, 0x5f, 0x55, 0x4e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x44, 0x10,
|
||||
0x1e, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45,
|
||||
0x5f, 0x41, 0x5a, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x50, 0x4f, 0x53, 0x10, 0x1f, 0x12, 0x18,
|
||||
0x0a, 0x14, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x54, 0x52,
|
||||
0x41, 0x56, 0x49, 0x53, 0x43, 0x49, 0x10, 0x20, 0x12, 0x17, 0x0a, 0x13, 0x53, 0x4f, 0x55, 0x52,
|
||||
0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x4f, 0x53, 0x54, 0x4d, 0x41, 0x4e, 0x10,
|
||||
0x21, 0x12, 0x17, 0x0a, 0x13, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45,
|
||||
0x5f, 0x57, 0x45, 0x42, 0x48, 0x4f, 0x4f, 0x4b, 0x10, 0x22, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x4f,
|
||||
0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x4c, 0x41, 0x53, 0x54, 0x49,
|
||||
0x43, 0x53, 0x45, 0x41, 0x52, 0x43, 0x48, 0x10, 0x23, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x4f, 0x55,
|
||||
0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x48, 0x55, 0x47, 0x47, 0x49, 0x4e, 0x47,
|
||||
0x46, 0x41, 0x43, 0x45, 0x10, 0x24, 0x12, 0x23, 0x0a, 0x1f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45,
|
||||
0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x48, 0x55, 0x42, 0x5f, 0x45, 0x58, 0x50,
|
||||
0x45, 0x52, 0x49, 0x4d, 0x45, 0x4e, 0x54, 0x41, 0x4c, 0x10, 0x25, 0x12, 0x16, 0x0a, 0x12, 0x53,
|
||||
0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x45, 0x4e, 0x54, 0x52,
|
||||
0x59, 0x10, 0x26, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59,
|
||||
0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x48, 0x55, 0x42, 0x5f, 0x52, 0x45, 0x41, 0x4c, 0x54, 0x49,
|
||||
0x4d, 0x45, 0x10, 0x27, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54,
|
||||
0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, 0x44, 0x49, 0x4e, 0x10, 0x28, 0x12, 0x20, 0x0a, 0x1c, 0x53,
|
||||
0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x4c, 0x41, 0x43, 0x4b,
|
||||
0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x49, 0x4e, 0x55, 0x4f, 0x55, 0x53, 0x10, 0x29, 0x12, 0x1f, 0x0a,
|
||||
0x1b, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4a, 0x53, 0x4f,
|
||||
0x4e, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x41, 0x54, 0x4f, 0x52, 0x10, 0x2a, 0x12, 0x23,
|
||||
0x0a, 0x1f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x57,
|
||||
0x53, 0x5f, 0x53, 0x45, 0x43, 0x52, 0x45, 0x54, 0x53, 0x5f, 0x4d, 0x41, 0x4e, 0x41, 0x47, 0x45,
|
||||
0x52, 0x10, 0x2b, 0x2a, 0x47, 0x0a, 0x19, 0x42, 0x69, 0x74, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74,
|
||||
0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65,
|
||||
0x12, 0x0e, 0x0a, 0x0a, 0x41, 0x55, 0x54, 0x4f, 0x44, 0x45, 0x54, 0x45, 0x43, 0x54, 0x10, 0x00,
|
||||
0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x55, 0x44, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x44,
|
||||
0x41, 0x54, 0x41, 0x5f, 0x43, 0x45, 0x4e, 0x54, 0x45, 0x52, 0x10, 0x02, 0x2a, 0x87, 0x01, 0x0a,
|
||||
0x14, 0x4a, 0x69, 0x72, 0x61, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x21, 0x4a, 0x49, 0x52, 0x41, 0x5f, 0x49, 0x4e,
|
||||
0x53, 0x54, 0x41, 0x4c, 0x4c, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f,
|
||||
0x41, 0x55, 0x54, 0x4f, 0x44, 0x45, 0x54, 0x45, 0x43, 0x54, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c,
|
||||
0x4a, 0x49, 0x52, 0x41, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4c, 0x4c, 0x41, 0x54, 0x49, 0x4f,
|
||||
0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4c, 0x4f, 0x55, 0x44, 0x10, 0x01, 0x12, 0x26,
|
||||
0x0a, 0x22, 0x4a, 0x49, 0x52, 0x41, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4c, 0x4c, 0x41, 0x54,
|
||||
0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x43, 0x45,
|
||||
0x4e, 0x54, 0x45, 0x52, 0x10, 0x02, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
|
||||
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x75,
|
||||
0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f,
|
||||
0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x45, 0x6e,
|
||||
0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2f, 0x0a, 0x13, 0x69, 0x6e,
|
||||
0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65,
|
||||
0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x31, 0x0a, 0x14, 0x69,
|
||||
0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65,
|
||||
0x6e, 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x69, 0x6e, 0x63, 0x6c, 0x75,
|
||||
0x64, 0x65, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2b,
|
||||
0x0a, 0x11, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x77, 0x6f,
|
||||
0x72, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x74, 0x65, 0x63,
|
||||
0x74, 0x6f, 0x72, 0x4b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x77,
|
||||
0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x0b,
|
||||
0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x50,
|
||||
0x61, 0x74, 0x68, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f,
|
||||
0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x74, 0x68, 0x73, 0x12,
|
||||
0x2b, 0x0a, 0x11, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x70,
|
||||
0x61, 0x74, 0x68, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x65, 0x6e, 0x76, 0x69,
|
||||
0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x73, 0x42, 0x0c, 0x0a, 0x0a,
|
||||
0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0xac, 0x01, 0x0a, 0x07, 0x57,
|
||||
0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x2e, 0x0a, 0x0e, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e,
|
||||
0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07,
|
||||
0xfa, 0x42, 0x04, 0x72, 0x02, 0x68, 0x01, 0x52, 0x0d, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x41,
|
||||
0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74,
|
||||
0x69, 0x61, 0x6c, 0x73, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x48, 0x00, 0x52, 0x06, 0x68,
|
||||
0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x06, 0x76, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e,
|
||||
0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x48, 0x01, 0x52, 0x06, 0x76, 0x65, 0x63, 0x74, 0x6f, 0x72,
|
||||
0x42, 0x0c, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x42, 0x09,
|
||||
0x0a, 0x07, 0x76, 0x61, 0x72, 0x69, 0x61, 0x6e, 0x74, 0x22, 0x4e, 0x0a, 0x06, 0x56, 0x65, 0x63,
|
||||
0x74, 0x6f, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x66,
|
||||
0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x6f, 0x63, 0x61,
|
||||
0x74, 0x6f, 0x72, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x6e, 0x6b,
|
||||
0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c,
|
||||
0x69, 0x6e, 0x6b, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0xcd, 0x02, 0x0a, 0x0d, 0x45, 0x6c,
|
||||
0x61, 0x73, 0x74, 0x69, 0x63, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x6e,
|
||||
0x6f, 0x64, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x6f, 0x64, 0x65,
|
||||
0x73, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a,
|
||||
0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x6c, 0x6f,
|
||||
0x75, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6c, 0x6f,
|
||||
0x75, 0x64, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79, 0x18,
|
||||
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a,
|
||||
0x0d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x06,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x6f, 0x6b,
|
||||
0x65, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x70, 0x61, 0x74, 0x74,
|
||||
0x65, 0x72, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x64, 0x65, 0x78,
|
||||
0x50, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79,
|
||||
0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x71, 0x75, 0x65,
|
||||
0x72, 0x79, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x5f,
|
||||
0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0e, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12,
|
||||
0x28, 0x0a, 0x10, 0x62, 0x65, 0x73, 0x74, 0x5f, 0x65, 0x66, 0x66, 0x6f, 0x72, 0x74, 0x5f, 0x73,
|
||||
0x63, 0x61, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x62, 0x65, 0x73, 0x74, 0x45,
|
||||
0x66, 0x66, 0x6f, 0x72, 0x74, 0x53, 0x63, 0x61, 0x6e, 0x22, 0xde, 0x01, 0x0a, 0x06, 0x53, 0x65,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
|
||||
0x12, 0x1f, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65,
|
||||
0x6e, 0x12, 0x19, 0x0a, 0x07, 0x64, 0x73, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01,
|
||||
0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x64, 0x73, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x0a, 0x07,
|
||||
0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52,
|
||||
0x06, 0x61, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x18, 0x69, 0x6e, 0x73, 0x65, 0x63,
|
||||
0x75, 0x72, 0x65, 0x5f, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x5f,
|
||||
0x74, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x69, 0x6e, 0x73, 0x65, 0x63,
|
||||
0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x54, 0x6c, 0x73,
|
||||
0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x06, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x42, 0x0c, 0x0a, 0x0a,
|
||||
0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0x07, 0x0a, 0x05, 0x53, 0x74,
|
||||
0x64, 0x69, 0x6e, 0x22, 0x4e, 0x0a, 0x0f, 0x53, 0x6c, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x74,
|
||||
0x69, 0x6e, 0x75, 0x6f, 0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70,
|
||||
0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73,
|
||||
0x70, 0x61, 0x63, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63,
|
||||
0x74, 0x49, 0x64, 0x22, 0x26, 0x0a, 0x0e, 0x4a, 0x53, 0x4f, 0x4e, 0x45, 0x6e, 0x75, 0x6d, 0x65,
|
||||
0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x01,
|
||||
0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x2a, 0xe9, 0x09, 0x0a, 0x0a,
|
||||
0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x4f,
|
||||
0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x5a, 0x55, 0x52, 0x45, 0x5f,
|
||||
0x53, 0x54, 0x4f, 0x52, 0x41, 0x47, 0x45, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x4f, 0x55,
|
||||
0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x49, 0x54, 0x42, 0x55, 0x43, 0x4b,
|
||||
0x45, 0x54, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54,
|
||||
0x59, 0x50, 0x45, 0x5f, 0x43, 0x49, 0x52, 0x43, 0x4c, 0x45, 0x43, 0x49, 0x10, 0x02, 0x12, 0x1a,
|
||||
0x0a, 0x16, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4f,
|
||||
0x4e, 0x46, 0x4c, 0x55, 0x45, 0x4e, 0x43, 0x45, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x4f,
|
||||
0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x43, 0x4b, 0x45, 0x52,
|
||||
0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50,
|
||||
0x45, 0x5f, 0x45, 0x43, 0x52, 0x10, 0x05, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x4f, 0x55, 0x52, 0x43,
|
||||
0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x43, 0x53, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12,
|
||||
0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x48,
|
||||
0x55, 0x42, 0x10, 0x07, 0x12, 0x1a, 0x0a, 0x16, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54,
|
||||
0x59, 0x50, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x47, 0x49, 0x54, 0x10, 0x08,
|
||||
0x12, 0x16, 0x0a, 0x12, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f,
|
||||
0x47, 0x49, 0x54, 0x4c, 0x41, 0x42, 0x10, 0x09, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x4f, 0x55, 0x52,
|
||||
0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4a, 0x49, 0x52, 0x41, 0x10, 0x0a, 0x12, 0x24,
|
||||
0x0a, 0x20, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x50,
|
||||
0x4d, 0x5f, 0x55, 0x4e, 0x41, 0x55, 0x54, 0x48, 0x44, 0x5f, 0x50, 0x41, 0x43, 0x4b, 0x41, 0x47,
|
||||
0x45, 0x53, 0x10, 0x0b, 0x12, 0x25, 0x0a, 0x21, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54,
|
||||
0x59, 0x50, 0x45, 0x5f, 0x50, 0x59, 0x50, 0x49, 0x5f, 0x55, 0x4e, 0x41, 0x55, 0x54, 0x48, 0x44,
|
||||
0x5f, 0x50, 0x41, 0x43, 0x4b, 0x41, 0x47, 0x45, 0x53, 0x10, 0x0c, 0x12, 0x12, 0x0a, 0x0e, 0x53,
|
||||
0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x33, 0x10, 0x0d, 0x12,
|
||||
0x15, 0x0a, 0x11, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53,
|
||||
0x4c, 0x41, 0x43, 0x4b, 0x10, 0x0e, 0x12, 0x1a, 0x0a, 0x16, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45,
|
||||
0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d,
|
||||
0x10, 0x0f, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50,
|
||||
0x45, 0x5f, 0x47, 0x49, 0x54, 0x10, 0x10, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x4f, 0x55, 0x52, 0x43,
|
||||
0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x10, 0x11, 0x12, 0x1b, 0x0a,
|
||||
0x17, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x33, 0x5f,
|
||||
0x55, 0x4e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x44, 0x10, 0x12, 0x12, 0x2a, 0x0a, 0x26, 0x53, 0x4f,
|
||||
0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x48, 0x55, 0x42,
|
||||
0x5f, 0x55, 0x4e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44,
|
||||
0x5f, 0x4f, 0x52, 0x47, 0x10, 0x13, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45,
|
||||
0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x55, 0x49, 0x4c, 0x44, 0x4b, 0x49, 0x54, 0x45, 0x10,
|
||||
0x14, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45,
|
||||
0x5f, 0x47, 0x45, 0x52, 0x52, 0x49, 0x54, 0x10, 0x15, 0x12, 0x17, 0x0a, 0x13, 0x53, 0x4f, 0x55,
|
||||
0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4a, 0x45, 0x4e, 0x4b, 0x49, 0x4e, 0x53,
|
||||
0x10, 0x16, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50,
|
||||
0x45, 0x5f, 0x54, 0x45, 0x41, 0x4d, 0x53, 0x10, 0x17, 0x12, 0x21, 0x0a, 0x1d, 0x53, 0x4f, 0x55,
|
||||
0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4a, 0x46, 0x52, 0x4f, 0x47, 0x5f, 0x41,
|
||||
0x52, 0x54, 0x49, 0x46, 0x41, 0x43, 0x54, 0x4f, 0x52, 0x59, 0x10, 0x18, 0x12, 0x16, 0x0a, 0x12,
|
||||
0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x59, 0x53, 0x4c,
|
||||
0x4f, 0x47, 0x10, 0x19, 0x12, 0x27, 0x0a, 0x23, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54,
|
||||
0x59, 0x50, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54,
|
||||
0x5f, 0x4d, 0x4f, 0x4e, 0x49, 0x54, 0x4f, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x1a, 0x12, 0x1e, 0x0a,
|
||||
0x1a, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x4c, 0x41,
|
||||
0x43, 0x4b, 0x5f, 0x52, 0x45, 0x41, 0x4c, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x1b, 0x12, 0x1c, 0x0a,
|
||||
0x18, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x4f, 0x4f,
|
||||
0x47, 0x4c, 0x45, 0x5f, 0x44, 0x52, 0x49, 0x56, 0x45, 0x10, 0x1c, 0x12, 0x1a, 0x0a, 0x16, 0x53,
|
||||
0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x48, 0x41, 0x52, 0x45,
|
||||
0x50, 0x4f, 0x49, 0x4e, 0x54, 0x10, 0x1d, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x4f, 0x55, 0x52, 0x43,
|
||||
0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x43, 0x53, 0x5f, 0x55, 0x4e, 0x41, 0x55, 0x54,
|
||||
0x48, 0x45, 0x44, 0x10, 0x1e, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f,
|
||||
0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x5a, 0x55, 0x52, 0x45, 0x5f, 0x52, 0x45, 0x50, 0x4f, 0x53,
|
||||
0x10, 0x1f, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50,
|
||||
0x45, 0x5f, 0x54, 0x52, 0x41, 0x56, 0x49, 0x53, 0x43, 0x49, 0x10, 0x20, 0x12, 0x17, 0x0a, 0x13,
|
||||
0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x4f, 0x53, 0x54,
|
||||
0x4d, 0x41, 0x4e, 0x10, 0x21, 0x12, 0x17, 0x0a, 0x13, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f,
|
||||
0x54, 0x59, 0x50, 0x45, 0x5f, 0x57, 0x45, 0x42, 0x48, 0x4f, 0x4f, 0x4b, 0x10, 0x22, 0x12, 0x1d,
|
||||
0x0a, 0x19, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x4c,
|
||||
0x41, 0x53, 0x54, 0x49, 0x43, 0x53, 0x45, 0x41, 0x52, 0x43, 0x48, 0x10, 0x23, 0x12, 0x1b, 0x0a,
|
||||
0x17, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x48, 0x55, 0x47,
|
||||
0x47, 0x49, 0x4e, 0x47, 0x46, 0x41, 0x43, 0x45, 0x10, 0x24, 0x12, 0x23, 0x0a, 0x1f, 0x53, 0x4f,
|
||||
0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x48, 0x55, 0x42,
|
||||
0x5f, 0x45, 0x58, 0x50, 0x45, 0x52, 0x49, 0x4d, 0x45, 0x4e, 0x54, 0x41, 0x4c, 0x10, 0x25, 0x12,
|
||||
0x16, 0x0a, 0x12, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53,
|
||||
0x45, 0x4e, 0x54, 0x52, 0x59, 0x10, 0x26, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x4f, 0x55, 0x52, 0x43,
|
||||
0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x49, 0x54, 0x48, 0x55, 0x42, 0x5f, 0x52, 0x45,
|
||||
0x41, 0x4c, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x27, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x4f, 0x55, 0x52,
|
||||
0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, 0x44, 0x49, 0x4e, 0x10, 0x28, 0x12,
|
||||
0x20, 0x0a, 0x1c, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53,
|
||||
0x4c, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x49, 0x4e, 0x55, 0x4f, 0x55, 0x53, 0x10,
|
||||
0x29, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45,
|
||||
0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x45, 0x52, 0x41, 0x54, 0x4f, 0x52,
|
||||
0x10, 0x2a, 0x12, 0x23, 0x0a, 0x1f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50,
|
||||
0x45, 0x5f, 0x41, 0x57, 0x53, 0x5f, 0x53, 0x45, 0x43, 0x52, 0x45, 0x54, 0x53, 0x5f, 0x4d, 0x41,
|
||||
0x4e, 0x41, 0x47, 0x45, 0x52, 0x10, 0x2b, 0x2a, 0x47, 0x0a, 0x19, 0x42, 0x69, 0x74, 0x62, 0x75,
|
||||
0x63, 0x6b, 0x65, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x54, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x41, 0x55, 0x54, 0x4f, 0x44, 0x45, 0x54, 0x45,
|
||||
0x43, 0x54, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x55, 0x44, 0x10, 0x01, 0x12,
|
||||
0x0f, 0x0a, 0x0b, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x43, 0x45, 0x4e, 0x54, 0x45, 0x52, 0x10, 0x02,
|
||||
0x2a, 0x87, 0x01, 0x0a, 0x14, 0x4a, 0x69, 0x72, 0x61, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x21, 0x4a, 0x49, 0x52,
|
||||
0x41, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4c, 0x4c, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54,
|
||||
0x59, 0x50, 0x45, 0x5f, 0x41, 0x55, 0x54, 0x4f, 0x44, 0x45, 0x54, 0x45, 0x43, 0x54, 0x10, 0x00,
|
||||
0x12, 0x20, 0x0a, 0x1c, 0x4a, 0x49, 0x52, 0x41, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4c, 0x4c,
|
||||
0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4c, 0x4f, 0x55, 0x44,
|
||||
0x10, 0x01, 0x12, 0x26, 0x0a, 0x22, 0x4a, 0x49, 0x52, 0x41, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41,
|
||||
0x4c, 0x4c, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x41, 0x54,
|
||||
0x41, 0x5f, 0x43, 0x45, 0x4e, 0x54, 0x45, 0x52, 0x10, 0x02, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69,
|
||||
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65,
|
||||
0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65,
|
||||
0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -117,7 +117,7 @@ func newConnector(ctx context.Context, source *Source) (Connector, error) {
|
||||
switch cred := source.conn.GetCredential().(type) {
|
||||
case *sourcespb.GitHub_GithubApp:
|
||||
log.RedactGlobally(cred.GithubApp.GetPrivateKey())
|
||||
return NewAppConnector(ctx, apiEndpoint, cred.GithubApp)
|
||||
return NewAppConnector(ctx, apiEndpoint, cred.GithubApp, source.conn.GetScanAllInstallations())
|
||||
case *sourcespb.GitHub_BasicAuth:
|
||||
log.RedactGlobally(cred.BasicAuth.GetPassword())
|
||||
return NewBasicAuthConnector(ctx, apiEndpoint, source.conn.GetClonePath(), cred.BasicAuth)
|
||||
|
||||
@@ -45,10 +45,16 @@ var _ Connector = (*appConnector)(nil)
|
||||
|
||||
const githubHTTPTimeoutSeconds = 60
|
||||
|
||||
func NewAppConnector(ctx context.Context, apiEndpoint string, app *credentialspb.GitHubApp) (Connector, error) {
|
||||
installationID, err := strconv.ParseInt(app.InstallationId, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse app installation ID %q: %w", app.InstallationId, err)
|
||||
func NewAppConnector(ctx context.Context, apiEndpoint string, app *credentialspb.GitHubApp, scanAllInstallations bool) (Connector, error) {
|
||||
var installationID int64
|
||||
if app.InstallationId != "" {
|
||||
var err error
|
||||
installationID, err = strconv.ParseInt(app.InstallationId, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse app installation ID %q: %w", app.InstallationId, err)
|
||||
}
|
||||
} else if !scanAllInstallations {
|
||||
return nil, fmt.Errorf("githubApp.installationId is required unless scanAllInstallations is set")
|
||||
}
|
||||
|
||||
appID, err := strconv.ParseInt(app.AppId, 10, 64)
|
||||
@@ -75,25 +81,45 @@ func NewAppConnector(ctx context.Context, apiEndpoint string, app *credentialspb
|
||||
repoInstallationMap: make(map[string]int64),
|
||||
}
|
||||
|
||||
if _, err := connector.APIClientForInstallation(installationID); err != nil {
|
||||
return nil, fmt.Errorf("could not create API client for configured installation: %w", err)
|
||||
}
|
||||
// When no default installation is configured (installationID == 0, only
|
||||
// possible with scanAllInstallations), installations are discovered and
|
||||
// clients created lazily per-repo, so there is no default client to
|
||||
// validate up front.
|
||||
if installationID != 0 {
|
||||
if _, err := connector.APIClientForInstallation(installationID); err != nil {
|
||||
return nil, fmt.Errorf("could not create API client for configured installation: %w", err)
|
||||
}
|
||||
|
||||
if _, err := connector.graphqlClientForInstallation(ctx, installationID); err != nil {
|
||||
return nil, fmt.Errorf("error creating GraphQL client: %w", err)
|
||||
if _, err := connector.graphqlClientForInstallation(ctx, installationID); err != nil {
|
||||
return nil, fmt.Errorf("error creating GraphQL client: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return connector, nil
|
||||
}
|
||||
|
||||
func (c *appConnector) APIClient() *github.Client {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
// HasDefaultInstallation reports whether a default installation is
|
||||
// configured. False only when scanAllInstallations is set and
|
||||
// githubApp.installationId was omitted, in which case there is no
|
||||
// authoritative installation to fall back to for repos/gists discovered
|
||||
// outside any installation's own listing.
|
||||
func (c *appConnector) HasDefaultInstallation() bool {
|
||||
return c.installationID != 0
|
||||
}
|
||||
|
||||
if clients := c.clientsByInstallationID[c.installationID]; clients != nil {
|
||||
return clients.apiClient
|
||||
// APIClient returns the client for the connector's default installation.
|
||||
// When scanAllInstallations is configured without a githubApp.installationId
|
||||
// (installationID == 0), NewAppConnector does not pre-create this client, so
|
||||
// it is created lazily here rather than returning nil: several callers
|
||||
// (Validate, mapRemainingAccessibleRepos, member gist/repo lookups) use this
|
||||
// as a fallback client for repos/resources outside any installation listing
|
||||
// and call it unconditionally.
|
||||
func (c *appConnector) APIClient() *github.Client {
|
||||
client, err := c.APIClientForInstallation(c.installationID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *appConnector) APIClientForRepo(repoURL string) (*github.Client, error) {
|
||||
@@ -140,6 +166,15 @@ func (c *appConnector) graphqlClientForInstallation(ctx context.Context, install
|
||||
|
||||
func (c *appConnector) Clone(ctx context.Context, repoURL string, args ...string) (string, *gogit.Repository, error) {
|
||||
installID, _ := c.installationIDForRepo(repoURL)
|
||||
if installID == 0 {
|
||||
// installID falls back to the connector's default installationID
|
||||
// (see installationIDForRepo/ensureRepoInstallation) for repos
|
||||
// discovered outside any installation's repo listing, e.g. org
|
||||
// members' personal repos with scanUsers. That default is unset (0)
|
||||
// when scanAllInstallations is true and githubApp.installationId
|
||||
// was omitted, since no single installation is authoritative.
|
||||
return "", nil, fmt.Errorf("no GitHub App installation resolved for repo %q; set githubApp.installationId to a fallback installation to scan repos outside installation listings (e.g. member repos with scanUsers) together with scanAllInstallations", repoURL)
|
||||
}
|
||||
|
||||
// TODO: Check rate limit for this call.
|
||||
token, _, err := c.installationClient.Apps.CreateInstallationToken(
|
||||
@@ -216,14 +251,15 @@ func (c *appConnector) setRepoInstallationForWiki(repoURL string, installationID
|
||||
}
|
||||
}
|
||||
|
||||
// GraphQLClient returns the GraphQL client for the connector's default
|
||||
// installation, lazily creating it if needed. See APIClient for why this
|
||||
// cannot simply return nil when no default installation is configured.
|
||||
func (c *appConnector) GraphQLClient() *githubv4.Client {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if clients := c.clientsByInstallationID[c.installationID]; clients != nil {
|
||||
return clients.graphqlClient
|
||||
client, err := c.graphqlClientForInstallation(context.Background(), c.installationID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *appConnector) InstallationClient() *github.Client {
|
||||
|
||||
@@ -152,7 +152,7 @@ func TestNewAppConnectorDefaultAPIClientUsesConfiguredInstallation(t *testing.T)
|
||||
PrivateKey: string(privKey),
|
||||
InstallationId: "4242",
|
||||
AppId: "12345",
|
||||
})
|
||||
}, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, connector.APIClient())
|
||||
require.NotNil(t, connector.GraphQLClient())
|
||||
@@ -174,6 +174,75 @@ func TestNewAppConnectorDefaultAPIClientUsesConfiguredInstallation(t *testing.T)
|
||||
assert.Contains(t, tokenRequestPaths[0], "/app/installations/4242/access_tokens")
|
||||
}
|
||||
|
||||
func TestNewAppConnectorInstallationIDOptionalWithScanAllInstallations(t *testing.T) {
|
||||
privKey := generateTestPrivateKey(t)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode([]map[string]string{})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// scanAllInstallations=true with no installationId configured should succeed.
|
||||
connector, err := NewAppConnector(trContext.Background(), server.URL, &credentialspb.GitHubApp{
|
||||
PrivateKey: string(privKey),
|
||||
AppId: "12345",
|
||||
}, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, connector)
|
||||
|
||||
// Without scanAllInstallations, installationId is still required.
|
||||
_, err = NewAppConnector(trContext.Background(), server.URL, &credentialspb.GitHubApp{
|
||||
PrivateKey: string(privKey),
|
||||
AppId: "12345",
|
||||
}, false)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestAPIClientAndGraphQLClientNonNilWithoutDefaultInstallation guards against
|
||||
// a nil-pointer panic: APIClient/GraphQLClient must never return nil, since
|
||||
// callers like Validate, mapRemainingAccessibleRepos, and member gist/repo
|
||||
// lookups call them unconditionally and dereference the result. Before this
|
||||
// fix, an appConnector with no default installation (installationID == 0,
|
||||
// the case when scanAllInstallations is true and installationId is omitted)
|
||||
// returned nil from a bare map lookup instead of lazily creating a client.
|
||||
func TestAPIClientAndGraphQLClientNonNilWithoutDefaultInstallation(t *testing.T) {
|
||||
privKey := generateTestPrivateKey(t)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode([]map[string]string{})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
connector, err := NewAppConnector(trContext.Background(), server.URL, &credentialspb.GitHubApp{
|
||||
PrivateKey: string(privKey),
|
||||
AppId: "12345",
|
||||
}, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, connector.APIClient())
|
||||
assert.NotNil(t, connector.GraphQLClient())
|
||||
}
|
||||
|
||||
// TestCloneErrorsWithoutResolvedInstallation covers repos that fall back to
|
||||
// the connector's default installationID (e.g. member personal repos found
|
||||
// outside any installation's repo listing, see ensureRepoInstallation). When
|
||||
// scanAllInstallations is true and no githubApp.installationId is
|
||||
// configured, that default is unset (0); Clone must fail with an actionable
|
||||
// error instead of attempting to use installation ID 0.
|
||||
func TestCloneErrorsWithoutResolvedInstallation(t *testing.T) {
|
||||
connector := &appConnector{
|
||||
installationID: 0,
|
||||
clientsByInstallationID: make(map[int64]*appInstallationClients),
|
||||
repoInstallationMap: make(map[string]int64),
|
||||
}
|
||||
|
||||
_, _, err := connector.Clone(trContext.Background(), "https://github.com/some-member/personal-repo.git")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no GitHub App installation resolved")
|
||||
}
|
||||
|
||||
func TestAPIClientForInstallationUsesConfiguredClient(t *testing.T) {
|
||||
defaultClient := github.NewClient(nil)
|
||||
connector := &appConnector{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -130,12 +129,6 @@ type unitEnvelope struct {
|
||||
func (u unitEnvelope) SourceUnitID() (string, sources.SourceUnitKind) { return u.ID, u.Kind }
|
||||
func (u unitEnvelope) Display() string { return u.Name }
|
||||
|
||||
func decodeBase64(data []byte) ([]byte, error) {
|
||||
out := make([]byte, base64.StdEncoding.DecodedLen(len(data)))
|
||||
n, err := base64.StdEncoding.Decode(out, data)
|
||||
return out[:n], err
|
||||
}
|
||||
|
||||
func unmarshalSourceUnit[unitType sources.SourceUnit](data []byte) (unitType, error) {
|
||||
u := new(unitType)
|
||||
|
||||
@@ -171,20 +164,12 @@ func (s *Source) UnmarshalSourceUnit(data []byte) (sources.SourceUnit, error) {
|
||||
return env, nil
|
||||
case "repo":
|
||||
if len(env.UnitData) > 0 {
|
||||
decodedData, err := decodeBase64(env.UnitData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding repo unit data: %w", err)
|
||||
}
|
||||
return unmarshalSourceUnit[RepoUnit](decodedData)
|
||||
return unmarshalSourceUnit[RepoUnit](env.UnitData)
|
||||
}
|
||||
return RepoUnit{Name: env.Name, URL: env.ID}, nil
|
||||
case "gist":
|
||||
decodedData, err := decodeBase64(env.UnitData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding gist unit data: %w", err)
|
||||
}
|
||||
if len(env.UnitData) > 0 {
|
||||
return unmarshalSourceUnit[GistUnit](decodedData)
|
||||
return unmarshalSourceUnit[GistUnit](env.UnitData)
|
||||
}
|
||||
return GistUnit{Name: env.Name, URL: env.ID}, nil
|
||||
default:
|
||||
@@ -417,6 +402,17 @@ func (s *Source) Validate(ctx context.Context) []error {
|
||||
- Returns 401 for invalid credentials but works with no auth (as unauthenticated)
|
||||
- Doesn't consume API quota when called
|
||||
*/
|
||||
// When scanAllInstallations is set with no githubApp.installationId,
|
||||
// there's no default installation token to call RateLimit with (see
|
||||
// appConnector.HasDefaultInstallation). Validate the App credentials
|
||||
// directly against the app-level (JWT-authenticated) client instead.
|
||||
if connector, ok := s.connector.(*appConnector); ok && !connector.HasDefaultInstallation() {
|
||||
if _, _, err := connector.InstallationClient().Apps.Get(ctx, ""); err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, _, err := s.connector.APIClient().RateLimit.Get(ctx); err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
@@ -599,6 +595,9 @@ func (s *Source) ensureRepoInfoCache(ctx context.Context, repo string, reporter
|
||||
}
|
||||
|
||||
if !s.ignoreGists && isGistUrl(urlParts) {
|
||||
if connector, ok := s.connector.(*appConnector); ok && !connector.HasDefaultInstallation() {
|
||||
return repo, fmt.Errorf("cannot fetch gist %q: githubApp.installationId is required as a fallback installation to scan gists together with scanAllInstallations", repo)
|
||||
}
|
||||
// Cache gist info.
|
||||
for {
|
||||
gistID := extractGistID(urlParts)
|
||||
@@ -768,6 +767,15 @@ func (s *Source) enumerateWithApp(ctx context.Context, connector *appConnector,
|
||||
return err
|
||||
}
|
||||
ctx.Logger().Info("Scanning repos", "org_members", len(s.memberCache))
|
||||
if !connector.HasDefaultInstallation() {
|
||||
// Member repos/gists live outside any installation's own
|
||||
// listing, so there is no installation token to fetch them
|
||||
// with (see appConnector.HasDefaultInstallation). Skip with a
|
||||
// clear message instead of letting each per-member API call
|
||||
// fail against installation ID 0.
|
||||
ctx.Logger().Info("Skipping org member repos and gists: githubApp.installationId is required as a fallback installation to scan members together with scanAllInstallations", "org_members", len(s.memberCache))
|
||||
return nil
|
||||
}
|
||||
// TODO: Replace loop below with a call to s.addReposForMembers(ctx, reporter)
|
||||
for member := range s.memberCache {
|
||||
logger := ctx.Logger().WithValues("member", member)
|
||||
@@ -1145,6 +1153,10 @@ func (s *Source) mapRemainingAccessibleRepos(
|
||||
connector *appConnector,
|
||||
wantedRepos map[string]repoMappingRequest,
|
||||
) error {
|
||||
if !connector.HasDefaultInstallation() {
|
||||
return fmt.Errorf("cannot resolve %d configured repo(s) outside installation listings: githubApp.installationId is required as a fallback installation together with scanAllInstallations", len(wantedRepos))
|
||||
}
|
||||
|
||||
var errs error
|
||||
client := connector.APIClient()
|
||||
for _, requested := range wantedRepos {
|
||||
|
||||
@@ -382,7 +382,8 @@ func TestAppConnector_EnterpriseBaseURL(t *testing.T) {
|
||||
PrivateKey: privateKey,
|
||||
InstallationId: "1337",
|
||||
AppId: "4141",
|
||||
})
|
||||
},
|
||||
false)
|
||||
require.NoError(t, err)
|
||||
|
||||
appConn, ok := connector.(*appConnector)
|
||||
@@ -411,6 +412,40 @@ func TestAppConnector_EnterpriseBaseURL(t *testing.T) {
|
||||
"AppsTransport.BaseURL should be set to enterprise endpoint")
|
||||
}
|
||||
|
||||
// TestValidate_ScanAllInstallationsWithoutDefaultInstallation covers the
|
||||
// scanAllInstallations-without-githubApp.installationId case: there is no
|
||||
// default installation token to call RateLimit with (see
|
||||
// appConnector.HasDefaultInstallation), so Validate must fall back to
|
||||
// checking App credentials via the app-level (JWT) client instead of
|
||||
// erroring against a non-existent installation ID 0.
|
||||
func TestValidate_ScanAllInstallationsWithoutDefaultInstallation(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
gock.New("https://api.github.com").
|
||||
Get("/app/installations").
|
||||
Reply(200).
|
||||
JSON([]map[string]any{})
|
||||
gock.New("https://api.github.com").
|
||||
Get("^/app$").
|
||||
Reply(200).
|
||||
JSON(map[string]any{"id": 4141})
|
||||
|
||||
s := initTestSource(&sourcespb.GitHub{
|
||||
Endpoint: "https://api.github.com",
|
||||
ScanAllInstallations: true,
|
||||
Credential: &sourcespb.GitHub_GithubApp{
|
||||
GithubApp: &credentialspb.GitHubApp{
|
||||
PrivateKey: createPrivateKey(),
|
||||
AppId: "4141",
|
||||
},
|
||||
}})
|
||||
|
||||
errs := s.Validate(context.Background())
|
||||
assert.Empty(t, errs)
|
||||
assert.False(t, gock.HasUnmatchedRequest())
|
||||
assert.True(t, gock.IsDone())
|
||||
}
|
||||
|
||||
func TestAddOrgsByUser(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
@@ -25,9 +24,6 @@ func newMarshalledRepoUnit(t *testing.T, su sources.SourceUnit) []byte {
|
||||
unitData, err := json.Marshal(su)
|
||||
require.NoError(t, err)
|
||||
|
||||
// [CG] This additional base64 encoding mimics the pipeline's behavior
|
||||
unitData = []byte(base64.StdEncoding.EncodeToString(unitData))
|
||||
|
||||
out, err := json.Marshal(map[string]any{
|
||||
"id": id,
|
||||
"kind": string(kind),
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
@@ -35,6 +37,7 @@ type Source struct {
|
||||
// Ensure the Source satisfies the interfaces at compile time
|
||||
var _ sources.Source = (*Source)(nil)
|
||||
var _ sources.SourceUnitUnmarshaller = (*Source)(nil)
|
||||
var _ sources.SourceUnitEnumChunker = (*Source)(nil)
|
||||
|
||||
func (s *Source) Type() sourcespb.SourceType { return SourceType }
|
||||
func (s *Source) SourceID() sources.SourceID { return s.sourceId }
|
||||
@@ -73,7 +76,7 @@ func (s *Source) Chunks(
|
||||
return nil
|
||||
}
|
||||
s.SetProgressComplete(i, len(s.paths), fmt.Sprintf("Path: %s", path), "")
|
||||
if err := s.chunkJSONEnumerator(ctx, path, chunksChan); err != nil {
|
||||
if err := s.chunkJSONEnumerator(ctx, path, sources.ChanReporter{Ch: chunksChan}); err != nil {
|
||||
ctx.Logger().Error(err, "error scanning JSON enumerator", "path", path)
|
||||
}
|
||||
}
|
||||
@@ -81,6 +84,34 @@ func (s *Source) Chunks(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enumerate implements the SourceUnitEnumerator interface, reporting each
|
||||
// configured path as its own unit so paths can be chunked concurrently.
|
||||
func (s *Source) Enumerate(ctx context.Context, reporter sources.UnitReporter) error {
|
||||
for _, path := range s.paths {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if err := reporter.UnitErr(ctx, err); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
_ = f.Close()
|
||||
if err := reporter.UnitOk(ctx, sources.CommonSourceUnit{ID: path}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChunkUnit implements the SourceUnitChunker interface.
|
||||
func (s *Source) ChunkUnit(ctx context.Context, unit sources.SourceUnit, reporter sources.ChunkReporter) error {
|
||||
path, _ := unit.SourceUnitID()
|
||||
if err := s.chunkJSONEnumerator(ctx, path, reporter); err != nil {
|
||||
return reporter.ChunkErr(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type jsonEntry struct {
|
||||
Metadata json.RawMessage
|
||||
Data []byte
|
||||
@@ -142,13 +173,11 @@ func (e *jsonEntry) UnmarshalJSON(data []byte) error {
|
||||
func (s *Source) chunkJSONEnumeratorReader(
|
||||
ctx context.Context,
|
||||
input io.Reader,
|
||||
chunksChan chan *sources.Chunk,
|
||||
reporter sources.ChunkReporter,
|
||||
) error {
|
||||
decoder := json.NewDecoder(input)
|
||||
var entry jsonEntry
|
||||
|
||||
reporter := sources.ChanReporter{Ch: chunksChan}
|
||||
|
||||
for {
|
||||
if err := decoder.Decode(&entry); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
@@ -190,7 +219,7 @@ func (s *Source) chunkJSONEnumeratorReader(
|
||||
func (s *Source) chunkJSONEnumerator(
|
||||
ctx context.Context,
|
||||
path string,
|
||||
chunksChan chan *sources.Chunk,
|
||||
reporter sources.ChunkReporter,
|
||||
) error {
|
||||
ctx.Logger().V(3).Info("chunking JSON enumerator", "path", path)
|
||||
|
||||
@@ -200,5 +229,15 @@ func (s *Source) chunkJSONEnumerator(
|
||||
}
|
||||
defer func() { _ = enumeratorFile.Close() }()
|
||||
|
||||
return s.chunkJSONEnumeratorReader(ctx, enumeratorFile, chunksChan)
|
||||
if strings.HasSuffix(path, ".zst") || strings.HasSuffix(path, ".zstd") {
|
||||
r, err := zstd.NewReader(enumeratorFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open zstd reader: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
return s.chunkJSONEnumeratorReader(ctx, r, reporter)
|
||||
} else {
|
||||
return s.chunkJSONEnumeratorReader(ctx, enumeratorFile, reporter)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestScanEnumerator(t *testing.T) {
|
||||
defer close(chunksChan)
|
||||
ctx := context.WithLogger(t.Context(), logr.Discard())
|
||||
source := &Source{}
|
||||
workerError = source.chunkJSONEnumeratorReader(ctx, readJSON, chunksChan)
|
||||
workerError = source.chunkJSONEnumeratorReader(ctx, readJSON, sources.ChanReporter{Ch: chunksChan})
|
||||
}()
|
||||
|
||||
enc := json.NewEncoder(writeJSON)
|
||||
@@ -190,7 +190,7 @@ func TestScanEnumeratorAPKContentRouting(t *testing.T) {
|
||||
defer close(chunksChan)
|
||||
ctx := context.WithLogger(t.Context(), logr.Discard())
|
||||
source := &Source{}
|
||||
workerError = source.chunkJSONEnumeratorReader(ctx, readJSON, chunksChan)
|
||||
workerError = source.chunkJSONEnumeratorReader(ctx, readJSON, sources.ChanReporter{Ch: chunksChan})
|
||||
}()
|
||||
|
||||
// No filename is set on the record; routing is content-based only.
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# S3 source
|
||||
|
||||
Scans objects in S3 buckets, or in any S3-compatible service.
|
||||
|
||||
## Scan paths
|
||||
|
||||
The source implements both the plain `Source` interface and `SourceUnitEnumChunker`, where one unit is one bucket (plus the role used to reach it).
|
||||
|
||||
- `Chunks` walks every bucket in sequence.
|
||||
- `Enumerate` reports each bucket as a unit, and `ChunkUnit` scans one bucket.
|
||||
|
||||
Both end up in `scanBucket` -> `pageChunker`, so per-object behavior is identical either way. The CLI always takes the unit path, because `main.go` enables source units and the source supports them.
|
||||
|
||||
## Credentials
|
||||
|
||||
Set by which flags are present:
|
||||
|
||||
| Flags | Credentials used |
|
||||
| --- | --- |
|
||||
| none | anonymous, for public buckets only |
|
||||
| `--cloud-environment` | the AWS SDK's normal chain: env vars, `~/.aws/credentials`, instance metadata |
|
||||
| `--key` and `--secret` | those static credentials |
|
||||
| `--key`, `--secret`, `--session-token` | those temporary credentials |
|
||||
|
||||
The default is anonymous, not your local AWS profile. Scanning a private bucket without `--cloud-environment` or explicit keys gives `403 AccessDenied`.
|
||||
|
||||
`--role-arn` assumes a role before scanning, and can be repeated. With roles and no `--bucket`, the source tries every bucket each role can list, so `AccessDenied` on buckets a role cannot reach is expected and does not fail the scan.
|
||||
|
||||
## Choosing buckets
|
||||
|
||||
`--bucket` scans only the named buckets. `--ignore-bucket` scans everything else. They cannot be combined; doing so fails at startup. Bucket names are matched exactly, not as globs or prefixes.
|
||||
|
||||
## Choosing objects within a bucket
|
||||
|
||||
Four flags, all repeatable, all matched against the object key before the object is downloaded. A skipped object costs no `GetObject` request.
|
||||
|
||||
- `--include-prefix` / `--exclude-prefix` match a literal key prefix. They can be used together to scan a subtree while leaving part of it out, and an exclude match always wins.
|
||||
- `--include-extension` / `--exclude-extension` match the file extension, written without a leading dot. They cannot be used together, since listing what to scan already excludes everything else. Matching is case insensitive.
|
||||
|
||||
Prefixes are literal, so `--include-prefix=log` matches `logs/app.txt` and `logs-archive/app.txt` alike.
|
||||
|
||||
The two kinds are combined with AND: an object has to pass both to be scanned. Entries are trimmed, and blank ones are ignored.
|
||||
|
||||
Filtering happens after listing, so it saves `GetObject` requests but not `ListObjectsV2` requests. A bucket is still paginated in full even when a prefix covers a small part of it. Pushing prefixes into `ListObjectsV2Input.Prefix` would cut that too, and has not been done.
|
||||
|
||||
Three edge cases worth knowing:
|
||||
|
||||
- Only the last extension counts. `path.Ext("backup.tar.gz")` is `.gz`, so `--exclude-extension=tar.gz` never matches, while `--exclude-extension=gz` does.
|
||||
- A key with no extension, like `Makefile`, matches no extension entry, so `--include-extension` skips it and `--exclude-extension` keeps it.
|
||||
- A dotfile is treated as all extension. `.env` has extension `env`, so `--exclude-extension=env` skips it, and any `--include-extension` list that omits `env` skips it too. That is easy to do by accident, and `.env` files are a common source of secrets.
|
||||
|
||||
Filters apply to object keys only, not to files inside an archive. `--exclude-extension=mp4` skips `clip.mp4` in the bucket but not a `clip.mp4` packed inside `media.zip`, since archive contents are filtered by the shared deny list instead. Excluding `zip` avoids expanding the archive at all.
|
||||
|
||||
Prefixes are matched with `strings.HasPrefix`, so an exclude prefix naming one key also excludes anything that key is a prefix of: `--exclude-prefix=foo/bar.txt` excludes `foo/bar.txt.bak` as well. There is no exact-key match.
|
||||
|
||||
The same prefixes and extensions apply to every bucket in a scan. There is no way to scope a prefix to one bucket.
|
||||
|
||||
If a filter excludes every object in a bucket, the scan logs `Scanned no objects in bucket` with the count, so a mistyped prefix does not look like a clean scan of an empty bucket.
|
||||
|
||||
## Objects skipped regardless of configuration
|
||||
|
||||
- Glacier and Glacier Instant Retrieval storage classes
|
||||
- Larger than `--max-object-size`, which defaults to 250 MiB and cannot be raised above it
|
||||
- Empty
|
||||
- Extensions in the shared deny list in `pkg/common/vars.go`, which already covers images, audio, and video
|
||||
- Keys ending in `/`, which are directory markers
|
||||
|
||||
## Custom endpoints
|
||||
|
||||
`--endpoint` points the scan at an S3-compatible service instead of AWS S3. Setting it changes three things:
|
||||
|
||||
- The client uses path-style addressing, since these services rarely publish the wildcard DNS that virtual-hosted addressing needs.
|
||||
- Per-bucket region discovery is skipped. `GetBucketRegion` is AWS-only, and a custom endpoint serves its own buckets, so requests sign with `--region` instead.
|
||||
- Object links in finding metadata point at the endpoint rather than `amazonaws.com`, so they resolve the way the scan read them.
|
||||
|
||||
The endpoint is parsed in `Init`, so a malformed one fails at startup rather than as an SDK error mid-scan. A missing scheme is assumed to be `https`.
|
||||
|
||||
`--region` sets the region used to sign requests and defaults to `us-east-1`. On AWS each bucket's real region is discovered automatically, so it mostly matters with a custom endpoint.
|
||||
|
||||
## Resumption
|
||||
|
||||
`Checkpointer` tracks which objects in the current page of up to 1000 have finished, and records the highest consecutively completed key as `StartAfter` in `Progress.EncodedResumeInfo`, along with the bucket and role. An interrupted scan resumes from there.
|
||||
|
||||
Only consecutive completions are checkpointed. If objects 0 to 5 and 7 to 8 are done but 6 is not, the checkpoint stops at 5, so resuming may rescan a few objects but never misses one.
|
||||
|
||||
This matters when adding a new skip to `pageChunker`: a skipped object still has to call `UpdateObjectCompletion`. Skipping without it stalls the checkpoint at the first skipped object, and a resumed scan then redoes everything after it.
|
||||
|
||||
The OSS CLI does not persist progress between runs, so resumption only takes effect where something stores and returns `EncodedResumeInfo`.
|
||||
@@ -0,0 +1,132 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// objectFilter narrows a scan to the configured key prefixes and file extensions.
|
||||
// An object must pass both axes to be scanned.
|
||||
//
|
||||
// The two axes treat a populated include and exclude list differently. Prefixes
|
||||
// accept both at once, so a scan can cover a subtree minus one directory, and
|
||||
// exclusion wins on a key that matches both. Extensions accept only one, because
|
||||
// naming the extensions to scan already excludes every other one; both together is
|
||||
// rejected in Init. Buckets reject both lists too, so prefixes are the exception.
|
||||
//
|
||||
// The zero value and a nil *objectFilter both include everything.
|
||||
type objectFilter struct {
|
||||
includePrefixes []string
|
||||
excludePrefixes []string
|
||||
|
||||
// Lowercase extensions without a leading dot, sorted and deduplicated. At most
|
||||
// one is populated.
|
||||
includeExtensions []string
|
||||
excludeExtensions []string
|
||||
}
|
||||
|
||||
// newObjectFilter builds a filter from the configured lists. Extensions may be
|
||||
// written with or without a leading dot and in any case. Entries are trimmed and
|
||||
// blank ones dropped, since a padded entry would match nothing and an empty prefix
|
||||
// would match every key.
|
||||
func newObjectFilter(includePrefixes, excludePrefixes, includeExtensions, excludeExtensions []string) (*objectFilter, error) {
|
||||
// Compare the cleaned lists rather than the raw ones, so that a list holding
|
||||
// nothing but blanks does not count as populated.
|
||||
include, exclude := cleanExtensions(includeExtensions), cleanExtensions(excludeExtensions)
|
||||
if len(include) > 0 && len(exclude) > 0 {
|
||||
return nil, errors.New("either an extension include list or an extension exclude list can be specified, but not both")
|
||||
}
|
||||
|
||||
return &objectFilter{
|
||||
includePrefixes: cleanEntries(includePrefixes, strings.TrimSpace),
|
||||
excludePrefixes: cleanEntries(excludePrefixes, strings.TrimSpace),
|
||||
includeExtensions: include,
|
||||
excludeExtensions: exclude,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// shouldInclude reports whether the object key passes both filter axes.
|
||||
func (f *objectFilter) shouldInclude(key string) bool {
|
||||
if f == nil {
|
||||
return true
|
||||
}
|
||||
return f.passesPrefixes(key) && f.passesExtensions(key)
|
||||
}
|
||||
|
||||
func (f *objectFilter) passesPrefixes(key string) bool {
|
||||
for _, prefix := range f.excludePrefixes {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(f.includePrefixes) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, prefix := range f.includePrefixes {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// passesExtensions applies the extension axis. A key with no extension matches no
|
||||
// configured entry, so it fails a populated include list and passes an exclude list.
|
||||
func (f *objectFilter) passesExtensions(key string) bool {
|
||||
if len(f.includeExtensions) == 0 && len(f.excludeExtensions) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// path.Ext rather than filepath.Ext: S3 keys always separate on "/", whatever
|
||||
// the host OS uses.
|
||||
ext := normalizeExtension(path.Ext(key))
|
||||
if len(f.excludeExtensions) > 0 {
|
||||
return !slices.Contains(f.excludeExtensions, ext)
|
||||
}
|
||||
return slices.Contains(f.includeExtensions, ext)
|
||||
}
|
||||
|
||||
// isConfigured reports whether any filtering is in effect.
|
||||
func (f *objectFilter) isConfigured() bool {
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
return len(f.includePrefixes) > 0 || len(f.excludePrefixes) > 0 ||
|
||||
len(f.includeExtensions) > 0 || len(f.excludeExtensions) > 0
|
||||
}
|
||||
|
||||
// normalizeExtension trims either side of the dot, so that a padded configured
|
||||
// value cannot silently match nothing. It runs on configured values and on the
|
||||
// extension read from a key alike, keeping both sides of a comparison normalized
|
||||
// the same way.
|
||||
func normalizeExtension(ext string) string {
|
||||
return strings.ToLower(strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(ext), ".")))
|
||||
}
|
||||
|
||||
// cleanExtensions normalizes configured extensions and sorts them, so that logging
|
||||
// the effective filter is stable rather than following the order they were given.
|
||||
func cleanExtensions(values []string) []string {
|
||||
exts := cleanEntries(values, normalizeExtension)
|
||||
slices.Sort(exts)
|
||||
return slices.Compact(exts)
|
||||
}
|
||||
|
||||
// cleanEntries normalizes each entry and drops the ones that come out empty,
|
||||
// returning nil when nothing is left.
|
||||
func cleanEntries(values []string, normalize func(string) string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
kept := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if normalized := normalize(value); normalized != "" {
|
||||
kept = append(kept, normalized)
|
||||
}
|
||||
}
|
||||
if len(kept) == 0 {
|
||||
return nil
|
||||
}
|
||||
return kept
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestObjectFilter(t *testing.T, includePrefixes, excludePrefixes, includeExts, excludeExts []string) *objectFilter {
|
||||
t.Helper()
|
||||
|
||||
filter, err := newObjectFilter(includePrefixes, excludePrefixes, includeExts, excludeExts)
|
||||
require.NoError(t, err)
|
||||
return filter
|
||||
}
|
||||
|
||||
func TestObjectFilter_Unconfigured(t *testing.T) {
|
||||
filter := newTestObjectFilter(t, nil, nil, nil, nil)
|
||||
|
||||
assert.True(t, filter.shouldInclude("any/key.zip"))
|
||||
assert.True(t, filter.shouldInclude("Makefile"))
|
||||
assert.True(t, (*objectFilter)(nil).shouldInclude("any/key.zip"))
|
||||
}
|
||||
|
||||
func TestObjectFilter_ExcludePrefix(t *testing.T) {
|
||||
filter := newTestObjectFilter(t, nil, []string{"projects/abc/"}, nil, nil)
|
||||
|
||||
assert.False(t, filter.shouldInclude("projects/abc/config.yaml"))
|
||||
assert.True(t, filter.shouldInclude("projects/xyz/config.yaml"))
|
||||
}
|
||||
|
||||
func TestObjectFilter_IncludePrefix(t *testing.T) {
|
||||
filter := newTestObjectFilter(t, []string{"infra/"}, nil, nil, nil)
|
||||
|
||||
assert.True(t, filter.shouldInclude("infra/main.tf"))
|
||||
assert.False(t, filter.shouldInclude("media/clip.mp4"))
|
||||
|
||||
// A prefix is matched literally, so it is not confined to a path boundary.
|
||||
assert.True(t, newTestObjectFilter(t, []string{"log"}, nil, nil, nil).shouldInclude("logs-archive/app.txt"))
|
||||
}
|
||||
|
||||
func TestObjectFilter_IncludeAndExcludePrefixes(t *testing.T) {
|
||||
filter := newTestObjectFilter(t, []string{"src/"}, []string{"src/vendor/"}, nil, nil)
|
||||
|
||||
assert.True(t, filter.shouldInclude("src/main.go"))
|
||||
assert.False(t, filter.shouldInclude("src/vendor/dep.go"), "exclusion takes precedence")
|
||||
assert.False(t, filter.shouldInclude("docs/readme.md"), "matches no include prefix")
|
||||
}
|
||||
|
||||
func TestObjectFilter_ExcludeExtension(t *testing.T) {
|
||||
filter := newTestObjectFilter(t, nil, nil, nil, []string{"zip", ".MP4"})
|
||||
|
||||
assert.False(t, filter.shouldInclude("build/artifact.zip"))
|
||||
assert.False(t, filter.shouldInclude("build/ARTIFACT.ZIP"), "matching is case insensitive")
|
||||
assert.False(t, filter.shouldInclude("media/clip.mp4"), "a configured leading dot is tolerated")
|
||||
assert.True(t, filter.shouldInclude("infra/main.tf"))
|
||||
assert.True(t, filter.shouldInclude("Makefile"), "a key with no extension is not excluded")
|
||||
}
|
||||
|
||||
func TestObjectFilter_IncludeExtension(t *testing.T) {
|
||||
filter := newTestObjectFilter(t, nil, nil, []string{"tf", "yaml"}, nil)
|
||||
|
||||
assert.True(t, filter.shouldInclude("infra/main.tf"))
|
||||
assert.True(t, filter.shouldInclude("infra/vars.yaml"))
|
||||
assert.False(t, filter.shouldInclude("infra/state.json"))
|
||||
assert.False(t, filter.shouldInclude("Makefile"), "a key with no extension matches no entry")
|
||||
}
|
||||
|
||||
func TestObjectFilter_BothExtensionListsRejected(t *testing.T) {
|
||||
_, err := newObjectFilter(nil, nil, []string{"tf"}, []string{"zip"})
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestObjectFilter_AxesCombine(t *testing.T) {
|
||||
filter := newTestObjectFilter(t, []string{"src/"}, nil, nil, []string{"zip"})
|
||||
|
||||
assert.True(t, filter.shouldInclude("src/main.go"))
|
||||
assert.False(t, filter.shouldInclude("src/bundle.zip"), "passes the prefix axis, fails the extension axis")
|
||||
assert.False(t, filter.shouldInclude("docs/guide.md"), "passes the extension axis, fails the prefix axis")
|
||||
}
|
||||
|
||||
// Enterprise passes these lists as newline separated text, which can yield blank
|
||||
// entries. A blank exclude prefix would otherwise match every key and skip the
|
||||
// whole bucket.
|
||||
func TestObjectFilter_BlankEntriesIgnored(t *testing.T) {
|
||||
filter := newTestObjectFilter(t, nil, []string{""}, nil, []string{""})
|
||||
|
||||
assert.True(t, filter.shouldInclude("any/key.zip"))
|
||||
}
|
||||
|
||||
// A list holding only blanks is not a populated list, so it cannot conflict with
|
||||
// the opposite list.
|
||||
func TestObjectFilter_BlankExtensionListIsNotAConflict(t *testing.T) {
|
||||
filter := newTestObjectFilter(t, nil, nil, []string{""}, []string{"zip"})
|
||||
|
||||
assert.False(t, filter.shouldInclude("build/artifact.zip"))
|
||||
assert.True(t, filter.shouldInclude("infra/main.tf"))
|
||||
}
|
||||
|
||||
// Padded entries reach the filter from config files and shell arguments. Without
|
||||
// trimming they match nothing, so an exclusion the user configured would silently
|
||||
// do nothing.
|
||||
func TestObjectFilter_EntriesAreTrimmed(t *testing.T) {
|
||||
prefixFilter := newTestObjectFilter(t, nil, []string{"archive/\r"}, nil, nil)
|
||||
assert.False(t, prefixFilter.shouldInclude("archive/a.txt"))
|
||||
|
||||
extFilter := newTestObjectFilter(t, nil, nil, nil, []string{" zip "})
|
||||
assert.False(t, extFilter.shouldInclude("build/artifact.zip"))
|
||||
|
||||
dotPadded := newTestObjectFilter(t, nil, nil, nil, []string{". zip"})
|
||||
assert.False(t, dotPadded.shouldInclude("build/artifact.zip"))
|
||||
|
||||
whitespaceOnly := newTestObjectFilter(t, nil, []string{" "}, nil, nil)
|
||||
assert.True(t, whitespaceOnly.shouldInclude("any/key.txt"))
|
||||
}
|
||||
+75
-34
@@ -58,6 +58,8 @@ type Source struct {
|
||||
maxObjectSize int64
|
||||
// endpoint is the S3-compatible service to scan, or nil for AWS S3.
|
||||
endpoint *url.URL
|
||||
// objectFilter is never nil after Init.
|
||||
objectFilter *objectFilter
|
||||
}
|
||||
|
||||
// Ensure the Source satisfies the interfaces at compile time
|
||||
@@ -112,6 +114,25 @@ func (s *Source) Init(
|
||||
return errors.New("either a bucket include list or a bucket ignore list can be specified, but not both")
|
||||
}
|
||||
|
||||
filter, err := newObjectFilter(
|
||||
conn.GetIncludePrefixes(),
|
||||
conn.GetExcludePrefixes(),
|
||||
conn.GetIncludeExtensions(),
|
||||
conn.GetExcludeExtensions(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.objectFilter = filter
|
||||
|
||||
if filter.isConfigured() {
|
||||
ctx.Logger().V(1).Info("Object filter configured",
|
||||
"include_prefixes", filter.includePrefixes,
|
||||
"exclude_prefixes", filter.excludePrefixes,
|
||||
"include_extensions", filter.includeExtensions,
|
||||
"exclude_extensions", filter.excludeExtensions)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -285,8 +306,9 @@ type pageMetadata struct {
|
||||
|
||||
// processingState tracks the state of concurrent S3 object processing.
|
||||
type processingState struct {
|
||||
errorCount *sync.Map // Thread-safe map tracking errors per prefix
|
||||
objectCount *uint64 // Total number of objects processed
|
||||
errorCount *sync.Map // Thread-safe map tracking errors per prefix
|
||||
objectCount *uint64 // Total number of objects processed
|
||||
filteredCount *uint64 // Total number of objects excluded by the object filter
|
||||
}
|
||||
|
||||
// resumePosition tracks where to restart scanning S3 buckets and objects after an interruption.
|
||||
@@ -444,7 +466,7 @@ func (s *Source) scanBucket(
|
||||
|
||||
pageNumber := 1
|
||||
paginator := s3.NewListObjectsV2Paginator(regionalClient, input)
|
||||
var objectCount uint64
|
||||
var objectCount, filteredCount uint64
|
||||
for paginator.HasMorePages() {
|
||||
output, err := paginator.NextPage(ctx)
|
||||
if err != nil {
|
||||
@@ -471,13 +493,21 @@ func (s *Source) scanBucket(
|
||||
page: output,
|
||||
}
|
||||
processingState := processingState{
|
||||
errorCount: &errorCount,
|
||||
objectCount: &objectCount,
|
||||
errorCount: &errorCount,
|
||||
objectCount: &objectCount,
|
||||
filteredCount: &filteredCount,
|
||||
}
|
||||
s.pageChunker(ctx, pageMetadata, processingState, reporter, checkpointer)
|
||||
|
||||
pageNumber++
|
||||
}
|
||||
|
||||
// A filter that excludes everything otherwise looks exactly like a clean scan
|
||||
// of an empty bucket, so say so rather than finishing silently.
|
||||
if objectCount == 0 && filteredCount > 0 {
|
||||
ctx.Logger().Info("Scanned no objects in bucket", "excluded_by_object_filter", filteredCount)
|
||||
}
|
||||
|
||||
return objectCount
|
||||
}
|
||||
|
||||
@@ -542,59 +572,70 @@ func (s *Source) pageChunker(
|
||||
checkpointer.Reset() // Reset the checkpointer for each PAGE
|
||||
ctx = context.WithValues(ctx, "bucket", metadata.bucket, "page_number", metadata.pageNumber)
|
||||
for objIdx, obj := range metadata.page.Contents {
|
||||
ctx = context.WithValues(ctx, "key", *obj.Key, "size", *obj.Size)
|
||||
if common.IsDone(ctx) {
|
||||
octx := context.WithValues(ctx, "key", *obj.Key, "size", *obj.Size)
|
||||
if common.IsDone(octx) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip objects excluded by the configured prefixes or extensions.
|
||||
if !s.objectFilter.shouldInclude(*obj.Key) {
|
||||
atomic.AddUint64(state.filteredCount, 1)
|
||||
octx.Logger().V(5).Info("Skipping filtered object")
|
||||
s.metricsCollector.RecordObjectSkipped(metadata.bucket, "object_filter", float64(*obj.Size))
|
||||
if err := checkpointer.UpdateObjectCompletion(octx, objIdx, metadata.bucket, metadata.role, metadata.page.Contents); err != nil {
|
||||
octx.Logger().Error(err, "could not update progress for filtered object")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip GLACIER and GLACIER_IR objects.
|
||||
if obj.StorageClass == s3types.ObjectStorageClassGlacier || obj.StorageClass == s3types.ObjectStorageClassGlacierIr {
|
||||
ctx.Logger().V(5).Info("Skipping object in storage class", "storage_class", obj.StorageClass)
|
||||
octx.Logger().V(5).Info("Skipping object in storage class", "storage_class", obj.StorageClass)
|
||||
s.metricsCollector.RecordObjectSkipped(metadata.bucket, "storage_class", float64(*obj.Size))
|
||||
if err := checkpointer.UpdateObjectCompletion(ctx, objIdx, metadata.bucket, metadata.role, metadata.page.Contents); err != nil {
|
||||
ctx.Logger().Error(err, "could not update progress for glacier object")
|
||||
if err := checkpointer.UpdateObjectCompletion(octx, objIdx, metadata.bucket, metadata.role, metadata.page.Contents); err != nil {
|
||||
octx.Logger().Error(err, "could not update progress for glacier object")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Ignore large files.
|
||||
if *obj.Size > s.maxObjectSize {
|
||||
ctx.Logger().V(5).Info("Skipping large file", "max_object_size", s.maxObjectSize)
|
||||
octx.Logger().V(5).Info("Skipping large file", "max_object_size", s.maxObjectSize)
|
||||
s.metricsCollector.RecordObjectSkipped(metadata.bucket, "size_limit", float64(*obj.Size))
|
||||
if err := checkpointer.UpdateObjectCompletion(ctx, objIdx, metadata.bucket, metadata.role, metadata.page.Contents); err != nil {
|
||||
ctx.Logger().Error(err, "could not update progress for large file")
|
||||
if err := checkpointer.UpdateObjectCompletion(octx, objIdx, metadata.bucket, metadata.role, metadata.page.Contents); err != nil {
|
||||
octx.Logger().Error(err, "could not update progress for large file")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// File empty file.
|
||||
if *obj.Size == 0 {
|
||||
ctx.Logger().V(5).Info("Skipping empty file")
|
||||
octx.Logger().V(5).Info("Skipping empty file")
|
||||
s.metricsCollector.RecordObjectSkipped(metadata.bucket, "empty_file", 0)
|
||||
if err := checkpointer.UpdateObjectCompletion(ctx, objIdx, metadata.bucket, metadata.role, metadata.page.Contents); err != nil {
|
||||
ctx.Logger().Error(err, "could not update progress for empty file")
|
||||
if err := checkpointer.UpdateObjectCompletion(octx, objIdx, metadata.bucket, metadata.role, metadata.page.Contents); err != nil {
|
||||
octx.Logger().Error(err, "could not update progress for empty file")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip incompatible extensions.
|
||||
if common.SkipFile(*obj.Key) {
|
||||
ctx.Logger().V(5).Info("Skipping file with incompatible extension")
|
||||
octx.Logger().V(5).Info("Skipping file with incompatible extension")
|
||||
s.metricsCollector.RecordObjectSkipped(metadata.bucket, "incompatible_extension", float64(*obj.Size))
|
||||
if err := checkpointer.UpdateObjectCompletion(ctx, objIdx, metadata.bucket, metadata.role, metadata.page.Contents); err != nil {
|
||||
ctx.Logger().Error(err, "could not update progress for incompatible file")
|
||||
if err := checkpointer.UpdateObjectCompletion(octx, objIdx, metadata.bucket, metadata.role, metadata.page.Contents); err != nil {
|
||||
octx.Logger().Error(err, "could not update progress for incompatible file")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
s.jobPool.Go(func() error {
|
||||
defer common.RecoverWithExit(ctx)
|
||||
if common.IsDone(ctx) {
|
||||
return ctx.Err()
|
||||
defer common.RecoverWithExit(octx)
|
||||
if common.IsDone(octx) {
|
||||
return octx.Err()
|
||||
}
|
||||
|
||||
if strings.HasSuffix(*obj.Key, "/") {
|
||||
ctx.Logger().V(5).Info("Skipping directory")
|
||||
octx.Logger().V(5).Info("Skipping directory")
|
||||
s.metricsCollector.RecordObjectSkipped(metadata.bucket, "directory", float64(*obj.Size))
|
||||
return nil
|
||||
}
|
||||
@@ -607,13 +648,13 @@ func (s *Source) pageChunker(
|
||||
nErr = 0
|
||||
}
|
||||
if nErr.(int) > 3 {
|
||||
ctx.Logger().V(2).Info("Skipped due to excessive errors")
|
||||
octx.Logger().V(2).Info("Skipped due to excessive errors")
|
||||
return nil
|
||||
}
|
||||
// Make sure we use a separate context for the GetObjectWithContext call.
|
||||
// This ensures that the timeout is isolated and does not affect any downstream operations. (e.g. HandleFile)
|
||||
const getObjectTimeout = 30 * time.Second
|
||||
objCtx, cancel := context.WithTimeout(ctx, getObjectTimeout)
|
||||
objCtx, cancel := context.WithTimeout(octx, getObjectTimeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := metadata.client.GetObject(objCtx, &s3.GetObjectInput{
|
||||
@@ -622,10 +663,10 @@ func (s *Source) pageChunker(
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "AccessDenied") {
|
||||
ctx.Logger().Error(err, "could not get S3 object; access denied")
|
||||
octx.Logger().Error(err, "could not get S3 object; access denied")
|
||||
s.metricsCollector.RecordObjectSkipped(metadata.bucket, "access_denied", float64(*obj.Size))
|
||||
} else {
|
||||
ctx.Logger().Error(err, "could not get S3 object")
|
||||
octx.Logger().Error(err, "could not get S3 object")
|
||||
s.metricsCollector.RecordObjectError(metadata.bucket)
|
||||
}
|
||||
// According to the documentation for GetObjectWithContext,
|
||||
@@ -641,14 +682,14 @@ func (s *Source) pageChunker(
|
||||
nErr = 0
|
||||
}
|
||||
if nErr.(int) > 3 {
|
||||
ctx.Logger().V(3).Info("Skipped due to excessive errors")
|
||||
octx.Logger().V(3).Info("Skipped due to excessive errors")
|
||||
return nil
|
||||
}
|
||||
nErr = nErr.(int) + 1
|
||||
state.errorCount.Store(prefix, nErr)
|
||||
// too many consecutive errors on this page
|
||||
if nErr.(int) > 3 {
|
||||
ctx.Logger().V(2).Info("Too many consecutive errors, excluding prefix", "prefix", prefix)
|
||||
octx.Logger().V(2).Info("Too many consecutive errors, excluding prefix", "prefix", prefix)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -678,13 +719,13 @@ func (s *Source) pageChunker(
|
||||
SourceVerify: s.verify,
|
||||
}
|
||||
|
||||
if err := handlers.HandleFile(ctx, res.Body, chunkSkel, reporter); err != nil {
|
||||
ctx.Logger().Error(err, "error handling file")
|
||||
if err := handlers.HandleFile(octx, res.Body, chunkSkel, reporter); err != nil {
|
||||
octx.Logger().Error(err, "error handling file")
|
||||
s.metricsCollector.RecordObjectError(metadata.bucket)
|
||||
return nil
|
||||
}
|
||||
atomic.AddUint64(state.objectCount, 1)
|
||||
ctx.Logger().V(5).Info("S3 object scanned.", "object_count", state.objectCount)
|
||||
octx.Logger().V(5).Info("S3 object scanned.", "object_count", state.objectCount)
|
||||
nErr, ok = state.errorCount.Load(prefix)
|
||||
if !ok {
|
||||
nErr = 0
|
||||
@@ -693,8 +734,8 @@ func (s *Source) pageChunker(
|
||||
state.errorCount.Store(prefix, 0)
|
||||
}
|
||||
// Update progress after successful processing.
|
||||
if err := checkpointer.UpdateObjectCompletion(ctx, objIdx, metadata.bucket, metadata.role, metadata.page.Contents); err != nil {
|
||||
ctx.Logger().Error(err, "could not update progress for scanned object")
|
||||
if err := checkpointer.UpdateObjectCompletion(octx, objIdx, metadata.bucket, metadata.role, metadata.page.Contents); err != nil {
|
||||
octx.Logger().Error(err, "could not update progress for scanned object")
|
||||
}
|
||||
s.metricsCollector.RecordObjectScanned(metadata.bucket, float64(*obj.Size))
|
||||
return nil
|
||||
|
||||
@@ -5,10 +5,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -438,3 +441,93 @@ func TestSource_ClientAddressing(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSource_Init_IncludeAndExcludeExtensionsError(t *testing.T) {
|
||||
conn, err := anypb.New(&sourcespb.S3{
|
||||
Credential: &sourcespb.S3_Unauthenticated{},
|
||||
IncludeExtensions: []string{"tf"},
|
||||
ExcludeExtensions: []string{"zip"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
s := Source{}
|
||||
err = s.Init(context.Background(), "s3 test source", 0, 0, false, conn, 1)
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// Prefixes, unlike extensions and unlike buckets, accept an include list and an
|
||||
// exclude list together.
|
||||
func TestSource_Init_IncludeAndExcludePrefixesAllowed(t *testing.T) {
|
||||
conn, err := anypb.New(&sourcespb.S3{
|
||||
Credential: &sourcespb.S3_Unauthenticated{},
|
||||
IncludePrefixes: []string{"src/"},
|
||||
ExcludePrefixes: []string{"src/vendor/"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
s := Source{}
|
||||
err = s.Init(context.Background(), "s3 test source", 0, 0, false, conn, 1)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, s.objectFilter.shouldInclude("src/main.tf"))
|
||||
assert.False(t, s.objectFilter.shouldInclude("src/vendor/dep.tf"))
|
||||
}
|
||||
|
||||
// A source with no filters configured must scan every object, so that existing
|
||||
// scans behave exactly as they did before object filtering was added.
|
||||
func TestSource_Init_UnconfiguredFilterScansEverything(t *testing.T) {
|
||||
conn, err := anypb.New(&sourcespb.S3{Credential: &sourcespb.S3_Unauthenticated{}})
|
||||
assert.NoError(t, err)
|
||||
|
||||
s := Source{}
|
||||
err = s.Init(context.Background(), "s3 test source", 0, 0, false, conn, 1)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, s.objectFilter.shouldInclude("any/key.zip"))
|
||||
}
|
||||
|
||||
// A filtered object must still be marked complete on the checkpointer. Otherwise
|
||||
// the low water mark stalls at the first filtered object and a resumed scan redoes
|
||||
// every object after it.
|
||||
func TestSource_PageChunker_FilteredObjectsAdvanceCheckpoint(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
conn, err := anypb.New(&sourcespb.S3{
|
||||
Credential: &sourcespb.S3_Unauthenticated{},
|
||||
ExcludePrefixes: []string{"archive/"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
s := Source{}
|
||||
require.NoError(t, s.Init(ctx, "s3 test source", 0, 0, false, conn, 1))
|
||||
|
||||
const objectCount = 10
|
||||
page := &awss3.ListObjectsV2Output{Contents: make([]s3types.Object, objectCount)}
|
||||
for i := range objectCount {
|
||||
key := fmt.Sprintf("archive/key-%02d.txt", i)
|
||||
size := int64(1024)
|
||||
page.Contents[i] = s3types.Object{Key: &key, Size: &size}
|
||||
}
|
||||
|
||||
checkpointer := NewCheckpointer(ctx, &sources.Progress{}, false)
|
||||
|
||||
// Every object is filtered, so pageChunker never reaches GetObject and needs
|
||||
// no client.
|
||||
var scanned, filtered uint64
|
||||
s.pageChunker(
|
||||
ctx,
|
||||
pageMetadata{bucket: "test-bucket", pageNumber: 1, page: page},
|
||||
processingState{errorCount: &sync.Map{}, objectCount: &scanned, filteredCount: &filtered},
|
||||
sources.ChanReporter{Ch: make(chan *sources.Chunk, objectCount)},
|
||||
checkpointer,
|
||||
)
|
||||
|
||||
assert.Zero(t, scanned)
|
||||
assert.EqualValues(t, objectCount, filtered)
|
||||
|
||||
resumeInfo, err := checkpointer.ResumePoint(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-bucket", resumeInfo.CurrentBucket)
|
||||
assert.Equal(t, *page.Contents[objectCount-1].Key, resumeInfo.StartAfter)
|
||||
}
|
||||
|
||||
@@ -421,6 +421,14 @@ type S3Config struct {
|
||||
Endpoint string
|
||||
// Region is the region used to sign requests.
|
||||
Region string
|
||||
// IncludePrefixes limits the scan to objects under these key prefixes.
|
||||
IncludePrefixes []string
|
||||
// ExcludePrefixes skips objects under these key prefixes.
|
||||
ExcludePrefixes []string
|
||||
// IncludeExtensions limits the scan to objects with these file extensions.
|
||||
IncludeExtensions []string
|
||||
// ExcludeExtensions skips objects with these file extensions.
|
||||
ExcludeExtensions []string
|
||||
}
|
||||
|
||||
// SyslogConfig defines the optional configuration for a syslog source.
|
||||
|
||||
@@ -1071,4 +1071,6 @@ enum DetectorType {
|
||||
NewRelicInsightsQueryKey = 1067;
|
||||
NewRelicMobileAppToken = 1068;
|
||||
SolarWindsObservability = 1069;
|
||||
HumioAPIToken = 1070;
|
||||
Resend = 1071;
|
||||
}
|
||||
|
||||
@@ -398,6 +398,20 @@ message S3 {
|
||||
string endpoint = 10 [(validate.rules).string.uri_ref = true];
|
||||
// region is the region used to sign requests. Defaults to us-east-1.
|
||||
string region = 11;
|
||||
// include_prefixes limits the scan to objects whose key matches one of these
|
||||
// key prefixes. Leave empty to consider every object in the bucket.
|
||||
repeated string include_prefixes = 12;
|
||||
// exclude_prefixes skips objects whose key matches one of these key prefixes.
|
||||
repeated string exclude_prefixes = 13;
|
||||
// include_extensions limits the scan to objects whose key ends in one of these
|
||||
// file extensions, written without a leading dot (e.g. "tf", "yaml"). Leave
|
||||
// empty to consider every extension. Cannot be combined with
|
||||
// exclude_extensions.
|
||||
repeated string include_extensions = 14;
|
||||
// exclude_extensions skips objects whose key ends in one of these file
|
||||
// extensions, written without a leading dot (e.g. "zip", "mp4"). Cannot be
|
||||
// combined with include_extensions.
|
||||
repeated string exclude_extensions = 15;
|
||||
}
|
||||
|
||||
message Slack {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
# NOTE: Version and args must match .github/workflows/lint.yml
|
||||
GOLANGCI_LINT_VERSION="v2.13.1"
|
||||
GOLANGCI_LINT_VERSION="v2.13.2"
|
||||
|
||||
LINT_ARGS="--enable bodyclose,copyloopvar,misspell --timeout 10m"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user